Files
osm-import-tools/generate-city-report.py
T
2026-08-28 14:23:26 -07:00

105 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""
Generate the City Boundaries report: addr:city agreement between the county
source and OpenStreetMap for already-matched addresses.
Reads addresses-existing.geojson (produced by compare-addresses.py, which
already carries county_postal_community/county_city/osm_city for each
matched pair) and classifies each point against OSM twice - once per county
field, since the postal community (USPS mailing city) and the legal
city/jurisdiction can disagree with each other, not just with OSM. No
spatial matching happens here, so this is fast/cheap to re-run on its own
without repeating the "Diff vs OSM" step.
Usage:
python generate-city-report.py \
--existing-file /data/lake/addresses-existing.geojson \
--output-dir /data/lake
"""
import argparse
import sys
import warnings
from pathlib import Path
import geopandas as gpd
import pandas as pd
warnings.filterwarnings('ignore')
def normalize_city(value) -> str:
if value is None or (isinstance(value, float) and pd.isna(value)):
return ''
s = str(value).strip()
return '' if s in ('', 'nan', 'None') else s
def agreement_status(county_city, osm_city) -> str:
c = normalize_city(county_city)
o = normalize_city(osm_city)
if not c and not o:
return 'both_missing'
if not c:
return 'osm_only'
if not o:
return 'county_only'
return 'match' if c.lower() == o.lower() else 'mismatch'
def main():
parser = argparse.ArgumentParser(
description="Generate the city-name agreement report from already-matched addresses",
epilog="Run compare-addresses.py first to produce addresses-existing.geojson."
)
parser.add_argument('--existing-file', required=True,
help='addresses-existing.geojson from compare-addresses.py')
parser.add_argument('--output-dir', '-o', required=True,
help='Output directory for city-comparison.geojson')
args = parser.parse_args()
existing_file = Path(args.existing_file)
output_dir = Path(args.output_dir)
if not existing_file.exists():
print(f"Error: {existing_file} not found.", file=sys.stderr)
print("Run 'Diff vs OSM' for addresses first.", file=sys.stderr)
return 1
gdf = gpd.read_file(existing_file)
required = ['county_postal_community', 'county_city', 'osm_city']
missing = [c for c in required if c not in gdf.columns]
if missing:
print(f"Error: {existing_file} is missing column(s): {', '.join(missing)}", file=sys.stderr)
print("This file predates the postal-community/city split - rerun "
"'Convert to GeoJSON' and 'Diff vs OSM' for this county "
"(convert-addresses.py needs to regenerate county-addresses.geojson "
"with the current field set) before generating the report.", file=sys.stderr)
return 1
gdf['status_postal_community'] = [
agreement_status(c, o)
for c, o in zip(gdf['county_postal_community'], gdf['osm_city'])
]
gdf['status_city'] = [
agreement_status(c, o)
for c, o in zip(gdf['county_city'], gdf['osm_city'])
]
output_dir.mkdir(parents=True, exist_ok=True)
out_path = output_dir / "city-comparison.geojson"
gdf.to_file(out_path, driver='GeoJSON')
print(f"Saved {len(gdf)} city comparisons to {out_path}")
for label, col in [('postal community', 'status_postal_community'), ('city', 'status_city')]:
print(f" vs {label}:")
for status, n in gdf[col].value_counts().items():
print(f" {status}: {n}")
return 0
if __name__ == '__main__':
sys.exit(main())