Create new project design plans
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Point-in-polygon check: for a hand-traced boundary (see the Boundary Tracer
|
||||
tool at /map-checker, or draw manually and save as boundary.geojson),
|
||||
flag every county/OSM address that disagrees with it - either inside the
|
||||
boundary but not labeled with the expected city, or outside it but labeled
|
||||
with the expected city anyway.
|
||||
|
||||
The boundary itself is drawn by a human looking at a reference map in their
|
||||
own browser (e.g. thevillages.com/map) - nothing here fetches from any
|
||||
third-party site, only our own /data/{county}/*.geojson files.
|
||||
|
||||
Usage:
|
||||
python check-boundary.py --county lake --county sumter
|
||||
python check-boundary.py --county lake --expect-city "The Villages" --output flagged.geojson
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from shapely.geometry import shape, Point
|
||||
from shapely.ops import unary_union
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
|
||||
def normalize_city(value):
|
||||
if value is None:
|
||||
return ''
|
||||
s = str(value).strip()
|
||||
return '' if s in ('', 'nan', 'None') else s
|
||||
|
||||
|
||||
def load_boundary(path):
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
polys = []
|
||||
for feat in data.get('features', []):
|
||||
name = (feat.get('properties') or {}).get('name', 'boundary')
|
||||
polys.append((name, shape(feat['geometry'])))
|
||||
return polys
|
||||
|
||||
|
||||
def load_addresses(data_dir, county):
|
||||
county_dir = Path(data_dir) / county
|
||||
# Prefer city-comparison.geojson: has postal community, city, AND osm_city
|
||||
# together for matched addresses. Falls back to county-addresses.geojson
|
||||
# (county-only, addr:city, no OSM side) if the diff step hasn't run yet.
|
||||
for filename in ('city-comparison.geojson', 'county-addresses.geojson'):
|
||||
path = county_dir / filename
|
||||
if path.exists():
|
||||
with open(path) as f:
|
||||
return json.load(f), filename
|
||||
return None, None
|
||||
|
||||
|
||||
def city_values(props):
|
||||
values = [
|
||||
normalize_city(props.get('county_postal_community')),
|
||||
normalize_city(props.get('county_city')),
|
||||
normalize_city(props.get('osm_city')),
|
||||
normalize_city(props.get('addr:city')),
|
||||
]
|
||||
return [v for v in values if v]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('--boundary', default='/data/map-checker/boundary.geojson',
|
||||
help='Path to the traced boundary GeoJSON (default: the file saved by '
|
||||
'the Boundary Tracer web tool at /map-checker)')
|
||||
parser.add_argument('--data-dir', default='/data')
|
||||
parser.add_argument('--county', action='append',
|
||||
help='County id to check (repeatable). Default: lake, sumter')
|
||||
parser.add_argument('--expect-city', default='The Villages',
|
||||
help='City name addresses inside the boundary should have (default: "The Villages")')
|
||||
parser.add_argument('--output', help='Optional path to save flagged addresses as GeoJSON')
|
||||
args = parser.parse_args()
|
||||
|
||||
boundary_path = Path(args.boundary)
|
||||
if not boundary_path.exists():
|
||||
print(f"Error: {boundary_path} not found.", file=sys.stderr)
|
||||
print("Trace a boundary first via the Boundary Tracer tool (/map-checker), "
|
||||
"or save one to that path by hand.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
polys = load_boundary(boundary_path)
|
||||
if not polys:
|
||||
print("Error: boundary file has no polygons.", file=sys.stderr)
|
||||
return 1
|
||||
combined = unary_union([g for _, g in polys])
|
||||
|
||||
counties = args.county or ['lake', 'sumter']
|
||||
expect = args.expect_city.strip().lower()
|
||||
|
||||
total = inside_count = inside_flagged = outside_flagged = 0
|
||||
flagged_features = []
|
||||
|
||||
print(f"Boundary: {', '.join(sorted(set(name for name, _ in polys)))} "
|
||||
f"({len(polys)} polygon(s))")
|
||||
|
||||
for county in counties:
|
||||
fc, source_file = load_addresses(args.data_dir, county)
|
||||
if fc is None:
|
||||
print(f" {county}: no data found, skipping.")
|
||||
continue
|
||||
|
||||
county_total = county_inside = county_inside_flagged = county_outside_flagged = 0
|
||||
|
||||
for feat in fc['features']:
|
||||
coords = feat['geometry']['coordinates']
|
||||
pt = Point(coords[0], coords[1])
|
||||
is_inside = combined.contains(pt)
|
||||
props = feat.get('properties', {})
|
||||
values = city_values(props)
|
||||
says_expected = any(v.lower() == expect for v in values)
|
||||
|
||||
county_total += 1
|
||||
if is_inside:
|
||||
county_inside += 1
|
||||
if values and not says_expected:
|
||||
county_inside_flagged += 1
|
||||
flagged_features.append({
|
||||
**feat,
|
||||
'properties': {**props, 'county': county, 'flag': 'inside_but_not_labeled'},
|
||||
})
|
||||
elif says_expected:
|
||||
county_outside_flagged += 1
|
||||
flagged_features.append({
|
||||
**feat,
|
||||
'properties': {**props, 'county': county, 'flag': 'outside_but_labeled'},
|
||||
})
|
||||
|
||||
print(f" {county} ({source_file}): {county_total} addresses, "
|
||||
f"{county_inside} inside boundary "
|
||||
f"({county_inside_flagged} not labeled '{args.expect_city}'), "
|
||||
f"{county_outside_flagged} outside boundary but labeled '{args.expect_city}'")
|
||||
|
||||
total += county_total
|
||||
inside_count += county_inside
|
||||
inside_flagged += county_inside_flagged
|
||||
outside_flagged += county_outside_flagged
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Total: {total} addresses checked, {inside_count} inside the boundary.")
|
||||
print(f" Inside but not labeled '{args.expect_city}': {inside_flagged}")
|
||||
print(f" Outside but labeled '{args.expect_city}': {outside_flagged}")
|
||||
|
||||
if args.output:
|
||||
out = {'type': 'FeatureCollection', 'features': flagged_features}
|
||||
with open(args.output, 'w') as f:
|
||||
json.dump(out, f)
|
||||
print(f"\nSaved {len(flagged_features)} flagged addresses to {args.output}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user