Add LIFECYCLE filtering, street exceptions UI, address matching improvements, map viewer fixes
This commit is contained in:
+66
-19
@@ -57,6 +57,20 @@ class AddressComparator:
|
||||
# 1 degree latitude ≈ 111,000 meters
|
||||
self.tolerance_deg = tolerance_meters / 111000.0
|
||||
|
||||
# Load street name exceptions from YAML
|
||||
exceptions_path = Path('/data/exceptions.yml')
|
||||
if exceptions_path.exists():
|
||||
import yaml
|
||||
with open(exceptions_path) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
self._exceptions = {
|
||||
item['from']: item['to']
|
||||
for item in data.get('corrections', [])
|
||||
if 'from' in item and 'to' in item
|
||||
}
|
||||
else:
|
||||
self._exceptions = {}
|
||||
|
||||
def _get_county_area_id(self, county: str, state: str) -> int:
|
||||
"""Get OSM area ID for a county using Nominatim."""
|
||||
search_query = f"{county} County, {state}, USA"
|
||||
@@ -230,12 +244,24 @@ out geom;"""
|
||||
|
||||
# Load and process shapefile
|
||||
gdf = gpd.read_file(shp_file)
|
||||
|
||||
|
||||
# Filter to active records only (Sumter uses LIFECYCLE field)
|
||||
LIFECYCLE_FIELD = 'LIFECYCLE'
|
||||
ACTIVE_VALUE = 'Current'
|
||||
if LIFECYCLE_FIELD in gdf.columns:
|
||||
before = len(gdf)
|
||||
gdf = gdf[gdf[LIFECYCLE_FIELD] == ACTIVE_VALUE].copy().reset_index(drop=True)
|
||||
filtered = before - len(gdf)
|
||||
if filtered:
|
||||
print(f"Filtered out {filtered} non-active addresses (LIFECYCLE != '{ACTIVE_VALUE}')")
|
||||
else:
|
||||
print(f"All {len(gdf)} addresses are active (LIFECYCLE == '{ACTIVE_VALUE}')")
|
||||
|
||||
# Convert CRS to WGS84 if needed
|
||||
if gdf.crs and gdf.crs != 'EPSG:4326':
|
||||
print(f"Converting from {gdf.crs} to EPSG:4326")
|
||||
gdf = gdf.to_crs('EPSG:4326')
|
||||
|
||||
|
||||
# Process address fields using existing logic from sumter-address-convert.py
|
||||
gdf = self._process_address_fields(gdf)
|
||||
|
||||
@@ -343,6 +369,13 @@ out geom;"""
|
||||
|
||||
address_mapping['addr:street'] = street_names
|
||||
|
||||
# Apply street name exceptions (e.g. county data quirks fixed for comparison/import)
|
||||
if 'addr:street' in address_mapping and self._exceptions:
|
||||
address_mapping['addr:street'] = [
|
||||
self._exceptions.get(name, name) if name is not None else None
|
||||
for name in address_mapping['addr:street']
|
||||
]
|
||||
|
||||
# City - try multiple field names
|
||||
city_fields = ['POST_COMM', 'PostalCity', 'CITY', 'Jurisdicti']
|
||||
for field in city_fields:
|
||||
@@ -375,6 +408,21 @@ out geom;"""
|
||||
|
||||
return processed_gdf
|
||||
|
||||
@staticmethod
|
||||
def _normalize_multi(value) -> str:
|
||||
"""Normalize a semicolon-delimited field by sorting its parts.
|
||||
|
||||
Treats '101;201;301' and '301;101;201' as equal — only real
|
||||
content differences (not ordering) count as added/removed.
|
||||
"""
|
||||
if value is None or (isinstance(value, float) and pd.isna(value)):
|
||||
return ''
|
||||
s = str(value).strip()
|
||||
if not s or s == 'nan':
|
||||
return ''
|
||||
parts = sorted(p.strip().lower() for p in s.split(';') if p.strip())
|
||||
return ';'.join(parts)
|
||||
|
||||
def _normalize_street_name(self, street: str) -> str:
|
||||
"""
|
||||
Normalize street names for better matching.
|
||||
@@ -464,8 +512,9 @@ out geom;"""
|
||||
distance = local_point.distance(osm_point)
|
||||
|
||||
# Verify house number, street, and unit (if present) match
|
||||
local_house_num = str(local_row.get('addr:housenumber', ''))
|
||||
osm_house_num = str(osm_row.get('addr:housenumber', ''))
|
||||
# _normalize_multi sorts semicolon-delimited values so order doesn't matter
|
||||
local_house_num = self._normalize_multi(local_row.get('addr:housenumber', ''))
|
||||
osm_house_num = self._normalize_multi(osm_row.get('addr:housenumber', ''))
|
||||
|
||||
# Check street name match (required) - use normalization for better matching
|
||||
local_street = self._normalize_street_name(str(local_row.get('addr:street', '')))
|
||||
@@ -473,16 +522,15 @@ out geom;"""
|
||||
street_match = (local_street == osm_street and local_street != '')
|
||||
|
||||
# Check unit match - if either has a unit, both must match
|
||||
local_unit = local_row.get('addr:unit')
|
||||
osm_unit = osm_row.get('addr:unit')
|
||||
local_unit_norm = self._normalize_multi(local_row.get('addr:unit'))
|
||||
osm_unit_norm = self._normalize_multi(osm_row.get('addr:unit'))
|
||||
|
||||
# Determine if each side has a unit
|
||||
local_has_unit = local_unit is not None and pd.notna(local_unit) and str(local_unit).strip() != ''
|
||||
osm_has_unit = osm_unit is not None and pd.notna(osm_unit) and str(osm_unit).strip() != ''
|
||||
local_has_unit = bool(local_unit_norm)
|
||||
osm_has_unit = bool(osm_unit_norm)
|
||||
|
||||
if local_has_unit and osm_has_unit:
|
||||
# Both have units - they must match
|
||||
unit_match = (str(local_unit).strip().lower() == str(osm_unit).strip().lower())
|
||||
# Both have units - they must match (order-insensitive)
|
||||
unit_match = (local_unit_norm == osm_unit_norm)
|
||||
elif local_has_unit or osm_has_unit:
|
||||
# One has unit, other doesn't - no match
|
||||
unit_match = False
|
||||
@@ -538,21 +586,20 @@ out geom;"""
|
||||
distance_meters = local_point.distance(osm_point) * 111000.0
|
||||
|
||||
# Verify house number, street, and unit (if present) match
|
||||
local_house_num = str(local_row.get('addr:housenumber', ''))
|
||||
osm_house_num = str(osm_row.get('addr:housenumber', ''))
|
||||
local_house_num = self._normalize_multi(local_row.get('addr:housenumber', ''))
|
||||
osm_house_num = self._normalize_multi(osm_row.get('addr:housenumber', ''))
|
||||
|
||||
# Check street name match (required) - use normalization for better matching
|
||||
local_street = self._normalize_street_name(str(local_row.get('addr:street', '')))
|
||||
osm_street = self._normalize_street_name(str(osm_row.get('addr:street', '')))
|
||||
street_match = (local_street == osm_street and local_street != '')
|
||||
|
||||
# Check unit match (only if both have units specified)
|
||||
local_unit = local_row.get('addr:unit')
|
||||
osm_unit = osm_row.get('addr:unit')
|
||||
# Check unit match (order-insensitive via _normalize_multi)
|
||||
local_unit_norm = self._normalize_multi(local_row.get('addr:unit'))
|
||||
osm_unit_norm = self._normalize_multi(osm_row.get('addr:unit'))
|
||||
unit_match = True
|
||||
if local_unit is not None and pd.notna(local_unit) and osm_unit is not None and pd.notna(osm_unit):
|
||||
# Both have units - they must match
|
||||
unit_match = (str(local_unit).strip().lower() == str(osm_unit).strip().lower())
|
||||
if local_unit_norm and osm_unit_norm:
|
||||
unit_match = (local_unit_norm == osm_unit_norm)
|
||||
|
||||
if (distance_meters <= self.tolerance_meters and
|
||||
local_house_num == osm_house_num and
|
||||
|
||||
Reference in New Issue
Block a user