Automating US County Boundary Extraction with OSMnx
TopologyException: side location conflict and empty GeoDataFrame returns are the two outputs that quietly break automated US county extraction with OSMnx — and both surface at the boundary-ingestion stage, before any siting, interconnection-queue, or setback math ever runs. OSMnx is architecturally optimized for street-network topology, but its geocode_to_gdf() helper is routinely repurposed to pull boundary=administrative polygons for energy-GIS work. At national scale that pattern triggers Nominatim rate limits, shapely topology exceptions, and silent CRS mismatches that corrupt downstream spatial joins and permitting calculations. This page is part of the Regulatory Boundary Mapping workflow within Core Energy-GIS Data & Spatial Fundamentals; it resolves each failure mode with root-cause mitigation, a pre-flight check, memory-aware batching, and authoritative fallback routing so the geometry entering your compliance mask is deterministic and audit-ready.
Root-Cause Analysis: Why OSMnx Fails on County Boundaries
OSMnx queries OpenStreetMap’s Nominatim geocoder, which returns community-edited boundary=administrative features. Three compounding causes dominate county-level extraction failures in energy pipelines, and none of them reliably raise on the first call:
- Ambiguous place queries. Nominatim returns multiple matches for generic county names (
"Washington County"resolves in 30+ states). Without an explicit state qualifier and a deterministicwhich_result, OSMnx silently returns the first hit or an emptyGeoDataFrame, contaminating siting models with the wrong polygon. - Topology exceptions. OSM administrative rings frequently contain self-intersections, duplicate nodes, and sliver polygons from edits. Passed to
geopandas.overlay()orshapely.intersection(), these raiseTopologyException: side location conflictand halt automated corridor or exclusion modeling. - CRS and area distortion. OSMnx returns
EPSG:4326(WGS84) by default. Computing acreage, setback buffers, or transmission right-of-way zones in unprojected degrees yields mathematically invalid results — degrees are not meters. This is the same projection-drift hazard covered under coordinate reference systems for energy projects.
The diagram below maps each cause to the fix stage that neutralizes it.
Pre-flight Validation: Surface the Root Cause Before Overlay
Run a cheap geocode probe before committing the polygon to your pipeline. This function surfaces ambiguity, emptiness, and invalidity up front, so the fault is logged at ingestion rather than masked as a vanished constraint three stages later.
import osmnx as ox
import geopandas as gpd
def preflight_county(query: str, state_abbr: str) -> dict:
"""Probe a county query and report the failure mode without raising mid-pipeline."""
full_query = f"{query} County, {state_abbr}, USA"
report = {"query": full_query, "ok": False, "reason": None, "n_features": 0}
try:
candidate = ox.geocode_to_gdf(full_query, which_result=1)
except Exception as exc: # Nominatim miss, 429, or network fault
report["reason"] = f"geocode_failed: {exc}"
return report
report["n_features"] = len(candidate)
if candidate.empty:
report["reason"] = "empty_result: no admin boundary matched the query"
return report
if not candidate.geometry.is_valid.all():
report["reason"] = "invalid_topology: self-intersection or sliver present"
return report
report["ok"] = True
return report
A clean run returns {"ok": True, ...}; anything else is a flag to route to a fallback source or tighten the query before the geometry pollutes a spatial join.
Fix Implementation: Deterministic Extraction with Topology Repair
The corrected extractor replaces fragile single-call logic with an explicit state-qualified query, make_valid topology repair, an empty-geometry guard, and an equal-area projection. EPSG:5070 (CONUS Albers Equal Area) is chosen deliberately: acreage and metric setbacks demand an equal-area projected CRS, never the source EPSG:4326.
import osmnx as ox
import geopandas as gpd
from shapely.validation import make_valid
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning)
def extract_validated_county(query: str, state_abbr: str, target_epsg: str = "EPSG:5070") -> gpd.GeoDataFrame:
"""Extract county geometry with deterministic query, topology repair, and equal-area projection."""
# 1. Deterministic Nominatim query with state qualifier
full_query = f"{query} County, {state_abbr}, USA"
county_gdf = ox.geocode_to_gdf(full_query, which_result=1)
# 2. Topology validation & repair (resolves side-location conflicts)
county_gdf["geometry"] = county_gdf["geometry"].apply(make_valid)
# 3. Drop invalid/empty geometries before projecting
valid_mask = county_gdf.geometry.is_valid & ~county_gdf.geometry.is_empty
county_gdf = county_gdf[valid_mask].copy()
if county_gdf.empty:
raise ValueError(f"No valid geometry returned for {full_query}.")
# 4. Project to CONUS Albers Equal Area for accurate acreage/buffer math
return county_gdf.to_crs(target_epsg)
Validate the area before trusting the polygon. Convert the projected area in square meters to acres with the exact survey-acre constant:
county_gdf = extract_validated_county("Riverside", "CA")
acres = county_gdf.area.sum() / 4046.8564224
Cross-check acres against the US Census reference for that FIPS code; a double-digit deviation almost always means the CRS step was skipped or the wrong polygon was geocoded. For systematic geometry checks across a batch, fold these assertions into your spatial data quality validation gates.
Fallback Routing & Performance Tuning
Batch extraction across 3,000+ counties requires chunked requests, aggressive caching, and strict memory control. Unmanaged loops trigger Nominatim 429 bans and exhaust RAM during geometry serialization.
import osmnx as ox
import geopandas as gpd
import pandas as pd
import time
import gc
from pathlib import Path
def batch_extract_counties(county_df: pd.DataFrame, output_dir: Path, chunk_size: int = 50) -> None:
"""Memory-optimized batch extraction with rate-limit compliance and fallback routing."""
# OSMnx >= 2.0 uses the settings object rather than the removed ox.config()
ox.settings.log_console = True
ox.settings.use_cache = True
ox.settings.cache_folder = str(Path(".osmnx_cache"))
for i in range(0, len(county_df), chunk_size):
chunk = county_df.iloc[i:i + chunk_size].copy()
valid_geoms = []
for _, row in chunk.iterrows():
try:
county_gdf = extract_validated_county(row["county_name"], row["state_abbr"])
valid_geoms.append(county_gdf)
except Exception as exc:
# Route to authoritative TIGER/Line or USGS when OSM is unusable
# https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html
print(f"[FALLBACK] OSM failed for {row['county_name']}, {row['state_abbr']}: {exc}")
continue
time.sleep(1.1) # Nominatim hard limit: 1 request/second
if valid_geoms:
chunk_gdf = gpd.GeoDataFrame(pd.concat(valid_geoms, ignore_index=True))
chunk_gdf.to_parquet(output_dir / f"chunk_{i:04d}.parquet", index=False)
del chunk_gdf
del valid_geoms
gc.collect() # Explicit reclamation for large geometry arrays
Apply these strategies when extraction runs at scale or inside CI/CD:
- Rate-limit compliance. Nominatim enforces 1 request/second; bursts earn IP bans that stall interconnection-queue refreshes. Keep the
time.sleep(1.1)throttle, or self-host a Nominatim instance for production volume. - Authoritative fallback registry. When OSM topology is irreparable, route to US Census TIGER/Line or a state GIS portal sourced through curated open energy data portals. Maintain a deterministic source registry so every fallback is reproducible.
- Chunked Parquet writes.
geopandasholds geometry arrays in memory until released; windowed writes to GeoParquet prevent OOM crashes over a full national run. - Persistent cache. Setting
ox.settings.use_cache = Truemakes re-runs idempotent and removes redundant Nominatim hits — critical for CI gates that re-execute on every commit. - Lock the projection once. Reproject at ingestion to
EPSG:5070, never per-operation, so buffer and overlay stages never re-trigger an implicit reproject. Centerline inputs that need buffering belong in Grid Capacity Buffer Analysis, not here.
Downstream Validation & Audit Trail
Energy developers and environmental teams need deterministic outputs for regulatory submissions. Gate every extracted batch through an assertion function before it feeds a compliance mask, and tag provenance so a permitting reviewer can trace each geometry to its source.
import geopandas as gpd
def audit_county_batch(gdf: gpd.GeoDataFrame, expected_epsg: int = 5070) -> None:
"""CI/CD-safe assertions on an extracted county batch."""
assert not gdf.empty, "empty batch — all geocodes failed or were dropped"
assert gdf.crs is not None and gdf.crs.to_epsg() == expected_epsg, \
f"CRS drift: expected EPSG:{expected_epsg}, got {gdf.crs}"
assert gdf.geometry.is_valid.all(), "invalid topology survived into the batch"
assert (gdf.geometry.geom_type.isin(["Polygon", "MultiPolygon"])).all(), \
"non-areal geometry present — masks require Polygon/MultiPolygon"
assert (gdf.area > 0).all(), "zero-area geometry indicates a failed projection"
Pair the assertions with provenance metadata on every output row — source="osm_nominatim", extraction_utc, and topology_repaired=True/False — and log the deviation reason, source URL, and a validation checksum whenever a county is served from a fallback dataset. That lineage is what makes the resulting mask defensible in a permitting audit, and it lets the parent Regulatory Boundary Mapping pipeline consume county geometry without ambiguity.
Related
- Regulatory Boundary Mapping — the parent workflow that consumes these validated county polygons.
- Coordinate Reference Systems for Energy Projects — projection choice behind the
EPSG:5070decision. - Spatial Data Quality Validation — systematic geometry and topology gates.
- Grid Capacity Buffer Analysis — where buffered linear features enter the compliance mask.
External Reference Standards
- OSMnx geocoding & configuration: https://osmnx.readthedocs.io/en/stable/
- Shapely geometry validation: https://shapely.readthedocs.io/en/stable/manual.html#shapely.validation.make_valid
- US Census TIGER/Line files: https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html