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:
data/cache/osm/bgcki.context_osm_highwaysThe tree-to-road join itself is no longer persisted as a separate cache file. It is computed in memory during criterion execution.
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 |
The preferred strategy combines ALKIS cadastral road polygons with OSM highway classifications:
AX_Flurstueck.zeigtAuf → AX_LagebezeichnungOhneHausnummer → lagebez_schl → street name CSVsjoin_nearest, using the already resolved OSM cache instead of a second live fetchThis combination offers the best of both worlds: precise official geometries + crowdsourced traffic hierarchy.
bluegreencity_ki/criteria/street_type.pytests/test_street_type.pyhighway column, reuse it directlyinputs.roads.source = local, load the configured local road datasetindex_right/index_left columns from previous criteria’s spatial joins (prevents sjoin_nearest conflicts)sjoin_nearest between trees and roads within configurable max_distanceNo 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:
street_type_risk: Normalized risk score (0-1) based on highway classificationstreet_type_value: Raw highway classification string (e.g., “motorway”, “residential”)dist_road_m: Distance from tree to nearest road in meters (computed during spatial join)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 |
Higher-order roads contribute to drought stress through:
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)
inputs:
roads:
source: "local"
path: "./data/input/local_roads.gpkg"
layer: "roads"
allowed: ["motorway", "trunk", "primary", "secondary", "tertiary", "residential"]
Restrict to major roads only:
inputs:
roads:
allowed: ["motorway", "trunk", "primary"]
This focuses on high-traffic roads, reducing false positives from quiet residential streets.
Use smaller max_distance to only consider very close roads:
inputs:
osm_join:
max_distance: 5.0 # Only roads within 5 meters
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
The street type criterion complements:
Run tests with:
conda activate bgcki_wesemann
python -m pytest tests/test_street_type.py -v
Tests cover:
After running the pipeline, validate results by:
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()}")