105 lines
3.6 KiB
Python
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())
|