bluegreencity-ki

Street Type Criterion

Overview

The street_type criterion evaluates drought risk based on pollution and heat exposure from nearby roads. Trees along high-traffic roads (motorways, major streets) face increased stress from vehicle emissions, road salt, sealed surfaces, and heat island effects. The criterion uses OpenStreetMap (OSM) highway classifications, ALKIS cadastral data (for more precise street names), or local road data to score risk dynamically based on road hierarchy.

Since the 2026-04 refactor, OSM roads are resolved through a central cache loader shared by the pipeline and the maintenance CLI:

The tree-to-road join itself is no longer persisted as a separate cache file. It is computed in memory during criterion execution.

Data Sources

Street geometry and classification can come from multiple sources, used in a fallback chain:

Strategy Source Precision Coverage Use Case
0 ALKIS + OSM Highest Niedersachsen ALKIS road polygons enriched with OSM highway classification
1 OSM file cache High Study area Fresh osm_highways_YYYYMM.gpkg in data/cache/osm/
2 PostGIS cache High Shared Fresh bgcki.context_osm_highways table
3 Local file Variable Custom User-provided GeoPackage/Shapefile
4 OSM Overpass Good Global Fresh download from OpenStreetMap

ALKIS + OSM Combination (Strategy 0)

The preferred strategy combines ALKIS cadastral road polygons with OSM highway classifications:

  1. ALKIS road polygons from LGLN WFS (Verkehrsflächen: funktion 5110, 5112) provide precise, officially surveyed road geometries
  2. ALKIS polygons are enriched with street names via the cadastral chain: AX_Flurstueck.zeigtAuf → AX_LagebezeichnungOhneHausnummer → lagebez_schl → street name CSV
  3. OSM highway types (motorway, trunk, primary, etc.) are spatially joined onto ALKIS polygons via sjoin_nearest, using the already resolved OSM cache instead of a second live fetch
  4. The result: official road geometries with OSM traffic classification

This combination offers the best of both worlds: precise official geometries + crowdsourced traffic hierarchy.

See: ALKIS Integration Guide

Implementation

Location

How It Works

  1. Road Acquisition:
    • if trees already carry a highway column, reuse it directly
    • else if inputs.roads.source = local, load the configured local road dataset
    • else resolve OSM roads via the central cache loader: file cache -> PostGIS cache -> fresh OSM fetch
    • if ALKIS is configured, enrich ALKIS road polygons with that resolved OSM hierarchy and prefer the ALKIS geometry for the join
  2. Column Cleanup: Drops leftover index_right/index_left columns from previous criteria’s spatial joins (prevents sjoin_nearest conflicts)
  3. Spatial Join: Performs sjoin_nearest between trees and roads within configurable max_distance
  4. Dynamic Risk Mapping: Maps highway types to risk scores based on configured road order
  5. Exclusion Logic: Trees without nearby roads (>max_distance) receive risk score 0.0

Scoring Logic

No nearby road (distance > max_distance) → Risk Score = 0.0 (no street pollution)
Motorway (highest traffic)               → Risk Score = 1.0 (highest risk)
Trunk road                               → Risk Score = 5/6 ≈ 0.83
Primary road                             → Risk Score = 4/6 ≈ 0.67
Secondary road                           → Risk Score = 3/6 = 0.50
Tertiary road                            → Risk Score = 2/6 ≈ 0.33
Residential street (lowest traffic)      → Risk Score = 1/6 ≈ 0.17 (lowest non-zero)
Missing highway data                     → Risk Score = 0.5 (neutral)

Note: The exact scores depend on the configured allowed road types. With 6 types (default), the formula is: risk = (n - rank) / n, where rank is the position in the allowed list (0-based).

Output Fields:

OSM Highway Classification

The criterion uses standard OSM highway tags:

Highway Type Description Typical Use Example
motorway High-speed divided highway Autobahn, Interstate A2, A7
trunk Important non-motorway road Bundesstraße B3, B6
primary Major road linking towns Hauptverkehrsstraße Vahrenwalder Str.
secondary Road linking neighborhoods Bezirksstraße Lister Meile
tertiary Minor road Wohngebietsstraße Small local roads
residential Street in residential area Anliegerstraße Quiet streets

Risk Rationale

Higher-order roads contribute to drought stress through:

Configuration

Basic Configuration

criteria:
  - name: "street_type"
    weight: 0.010783           # calibrated value; see config/config.yaml
    params: {}

inputs:
  roads:
    source: "osm"              # Use OpenStreetMap data
    cache_dir: "./data/cache/osm"
    allowed: ["motorway", "trunk", "primary", "secondary", "tertiary", "residential"]
    buffer_m: 200.0            # live config: enlarge bbox by 200 m (0 = exact study area)
    refresh_days: 365          # Cache freshness (1 year)
  
  osm_join:
    max_distance: 50.0         # live config: trees up to 50 m from a road are scored (code fallback: 10.0)

With Local Road Data

inputs:
  roads:
    source: "local"
    path: "./data/input/local_roads.gpkg"
    layer: "roads"
    allowed: ["motorway", "trunk", "primary", "secondary", "tertiary", "residential"]

Custom Allowed Roads

Restrict to major roads only:

inputs:
  roads:
    allowed: ["motorway", "trunk", "primary"]

This focuses on high-traffic roads, reducing false positives from quiet residential streets.

Tight Spatial Join

Use smaller max_distance to only consider very close roads:

inputs:
  osm_join:
    max_distance: 5.0          # Only roads within 5 meters

Custom Overpass Endpoint

For large study areas, use a custom Overpass API endpoint:

inputs:
  roads:
    overpass_endpoint: "https://overpass.kumi.systems/api/interpreter"

If neither the GeoPackage cache nor the PostGIS cache is fresh, new data is fetched from OSM and both caches are refreshed.

### Manual Cache Refresh

Delete the OSM cache files to force re-download:
```bash
rm ./data/cache/osm/osm_highways_*.gpkg

Integration with Other Criteria

The street type criterion complements:

Validation & Testing

Unit Tests

Run tests with:

conda activate bgcki_wesemann
python -m pytest tests/test_street_type.py -v

Tests cover:

Visual Validation

After running the pipeline, validate results by:

  1. Map overlay: Visualize trees colored by street_type score over OSM basemap
  2. Distance check: Plot distance to nearest road vs. risk score
  3. Highway distribution: Histogram of highway types
  4. Spatial patterns: Identify clusters of high-risk trees near motorways

Example Validation Code

import geopandas as gpd
import matplotlib.pyplot as plt
import contextily as ctx

# Load results
gdf = gpd.read_file("./data/output/<study_area>/trees_at_risk_<area>.gpkg", layer="trees_at_risk_points")

# Plot trees colored by street_type risk
fig, ax = plt.subplots(figsize=(12, 12))

# Plot trees
gdf.to_crs(epsg=3857).plot(
    column='street_type_risk',
    cmap='RdYlGn_r',  # Red = high risk, Green = low risk
    legend=True,
    ax=ax,
    markersize=5,
    alpha=0.7
)

# Add basemap
ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik)

ax.set_title('Street Type Risk Score')
ax.set_axis_off()
plt.tight_layout()
plt.show()

# Highway type distribution
highway_counts = gdf['street_type_value'].value_counts()
print("Highway Type Distribution:")
print(highway_counts)

# Score distribution by highway type
gdf.boxplot(column='street_type_risk', by='street_type_value', figsize=(12, 6))
plt.suptitle('Risk Score by Highway Type')
plt.xlabel('Highway Type')
plt.ylabel('Risk Score')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# Distance to road vs. score
if 'dist_road_m' in gdf.columns:
    plt.figure(figsize=(10, 6))
    plt.scatter(gdf['dist_road_m'], gdf['street_type_risk'], alpha=0.3)
    plt.xlabel('Distance to Nearest Road (m)')
    plt.ylabel('Street Type Risk Score')
    plt.title('Distance vs. Risk Score')
    plt.grid(True, alpha=0.3)
    plt.show()
    
    # Summary statistics for distance
    print("\nDistance to Road Statistics:")
    print(gdf['dist_road_m'].describe())
    print(f"Trees within 5m of road: {(gdf['dist_road_m'] <= 5).sum()}")
    print(f"Trees beyond max_distance: {gdf['dist_road_m'].isna().sum()}")

Known Limitations

  1. OSM data quality: OSM highway classifications may be incomplete or inconsistent
  2. Static road classification: Does not account for actual traffic volume (AADT)
  3. No temporal variation: Road risk is constant (no rush hour vs. night differences)
  4. Simple distance metric: Euclidean distance may not reflect actual exposure (wind direction, barriers)
  5. No road width: Narrow residential streets treated same as wide boulevards of same type

References

Changelog