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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user