Address rename detection with Accept Rename tag-edit export, hover-to-toggle snap dots, cross-layer click fix, fix county street naming (Saint, County Road, Parkway), and keep OSM ids in address downloads

This commit is contained in:
zyphlar
2026-10-21 18:14:33 -07:00
parent b44ec0341f
commit 28550984be
6 changed files with 443 additions and 98 deletions
+56
View File
@@ -20,6 +20,7 @@ from pathlib import Path
from typing import Dict, List, Tuple
import geopandas as gpd
import numpy as np
import pandas as pd
from shapely.strtree import STRtree
import warnings
@@ -28,6 +29,7 @@ warnings.filterwarnings('ignore')
CONFLICT_RADIUS_METERS = 5.0
RENAME_RADIUS_METERS = 100.0
class AddressComparator:
@@ -86,6 +88,53 @@ class AddressComparator:
break
addr['conflict'] = conflict
def _flag_possible_renames(self, new_addresses: List[Dict], removed_addresses: List[Dict],
radius_meters: float = RENAME_RADIUS_METERS) -> int:
"""Pair each new county address with an unmatched OSM address nearby that
has the same house number (and a compatible unit) but a different street
name, e.g. county "828 Donelin Circle" vs OSM "828 Donelin CR". Both sides
get 'possible_rename' set to the other side's street so the map can show
them as a likely rename/misnaming rather than an unrelated add + removal,
and a shared 'rename_pair' id so the map can accept both together.
Returns the number of pairs found."""
if not new_addresses or not removed_addresses:
return 0
geoms = [a['geometry'] for a in removed_addresses]
tree = STRtree(geoms)
radius_deg = radius_meters / 111000.0
paired = set()
count = 0
for addr in new_addresses:
num = self._normalize_multi(addr.get('addr:housenumber'))
street = self._normalize_street_name(str(addr.get('addr:street', '')))
unit = self._normalize_multi(addr.get('addr:unit'))
if not num or not street:
continue
best, best_dist = None, float('inf')
for j in tree.query(addr['geometry'].buffer(radius_deg)):
if j in paired:
continue
osm = removed_addresses[j]
if self._normalize_multi(osm.get('addr:housenumber')) != num:
continue
osm_street = self._normalize_street_name(str(osm.get('addr:street', '')))
if not osm_street or osm_street == street:
continue
osm_unit = self._normalize_multi(osm.get('addr:unit'))
if unit and osm_unit and unit != osm_unit:
continue
d = addr['geometry'].distance(geoms[j]) * 111000.0
if d <= radius_meters and d < best_dist:
best, best_dist = j, d
if best is not None:
paired.add(best)
osm = removed_addresses[best]
addr['possible_rename'] = str(osm.get('addr:street'))
osm['possible_rename'] = str(addr.get('addr:street'))
count += 1
addr['rename_pair'] = osm['rename_pair'] = count
return count
def compare_addresses(self, local_file: str, osm_file: str) -> Tuple[List[Dict], List[Dict], List[Dict]]:
"""Compare local and OSM address data.
@@ -197,10 +246,17 @@ class AddressComparator:
for idx, osm_row in osm_gdf.iterrows():
if idx not in matched_osm:
props = dict(osm_row.drop('geometry'))
# geopandas reads JSON int arrays (osm_nodes) as numpy arrays,
# which would be written back out as a string
for k, v in props.items():
if isinstance(v, np.ndarray):
props[k] = [int(x) for x in v.tolist()]
props['status'] = 'removed'
removed_addresses.append({'geometry': osm_row.geometry, **props})
self._flag_close_conflicts(new_addresses)
renames = self._flag_possible_renames(new_addresses, removed_addresses)
print(f"Possible street renames (same number nearby, different street): {renames}")
return new_addresses, existing_addresses, removed_addresses