diff --git a/compare-addresses.py b/compare-addresses.py index 0a1f60c..88ef42e 100644 --- a/compare-addresses.py +++ b/compare-addresses.py @@ -27,6 +27,9 @@ import warnings warnings.filterwarnings('ignore') +CONFLICT_RADIUS_METERS = 5.0 + + class AddressComparator: def __init__(self, tolerance_meters: float = 500.0): self.tolerance_meters = tolerance_meters @@ -62,6 +65,27 @@ class AddressComparator: s = re.sub(r'\s+', ' ', s).strip() return s + @staticmethod + def _flag_close_conflicts(addresses: List[Dict], radius_meters: float = CONFLICT_RADIUS_METERS) -> None: + """Mark each address dict's 'conflict' key True when another point in the + same list falls within radius_meters. Doesn't dedupe or exclude anything - + just surfaces tight clusters (e.g. ambiguous source data reusing the same + unit label across distinct points) for manual review in the map UI.""" + if not addresses: + return + geoms = [a['geometry'] for a in addresses] + tree = STRtree(geoms) + radius_deg = radius_meters / 111000.0 + for i, addr in enumerate(addresses): + conflict = False + for j in tree.query(geoms[i].buffer(radius_deg)): + if j == i: + continue + if geoms[i].distance(geoms[j]) * 111000.0 <= radius_meters: + conflict = True + break + addr['conflict'] = conflict + def compare_addresses(self, local_file: str, osm_file: str) -> Tuple[List[Dict], List[Dict], List[Dict]]: """Compare local and OSM address data. @@ -175,6 +199,8 @@ class AddressComparator: props['status'] = 'removed' removed_addresses.append({'geometry': osm_row.geometry, **props}) + self._flag_close_conflicts(new_addresses) + return new_addresses, existing_addresses, removed_addresses def save_results(self, new_addresses, existing_addresses, removed_addresses, output_dir): diff --git a/convert-addresses.py b/convert-addresses.py index b9d7d16..11a6d2a 100644 --- a/convert-addresses.py +++ b/convert-addresses.py @@ -160,12 +160,6 @@ def convert(zip_path, output_path): zip_path = Path(zip_path) output_path = Path(output_path) - # Skip if output is newer than input - if (output_path.exists() and zip_path.exists() and - output_path.stat().st_mtime > zip_path.stat().st_mtime): - print(f"Output is up to date: {output_path}") - return - print(f"Converting {zip_path} ...") exceptions = load_exceptions() diff --git a/web/static/map.js b/web/static/map.js index c87f6e1..b388e14 100644 --- a/web/static/map.js +++ b/web/static/map.js @@ -312,8 +312,10 @@ function diffStyle(feature) { } const isRemoved = feature.properties && (feature.properties.removed === true || feature.properties.removed === 'True'); + const isConflicted = !isRemoved && feature.properties && + (feature.properties.conflict === true || feature.properties.conflict === 'True'); return { - color: isRemoved ? '#ff0000' : '#00ff00', + color: isRemoved ? '#ff0000' : (isConflicted ? '#c9a800' : '#00ff00'), weight: 3, opacity: 0.8 }; @@ -344,10 +346,12 @@ function diffMarkerStyle(feature) { } const isRemoved = feature.properties && (feature.properties.removed === true || feature.properties.removed === 'True'); + const isConflicted = !isRemoved && feature.properties && + (feature.properties.conflict === true || feature.properties.conflict === 'True'); return { radius: 6, - fillColor: isRemoved ? '#ff0000' : '#00ff00', - color: isRemoved ? '#cc0000' : '#00cc00', + fillColor: isRemoved ? '#ff0000' : (isConflicted ? '#ffeb3b' : '#00ff00'), + color: isRemoved ? '#cc0000' : (isConflicted ? '#c9a800' : '#00cc00'), weight: 1, opacity: 0.8, fillOpacity: 0.7 @@ -457,17 +461,20 @@ function shouldShowFeature(feature) { if (isExcluded(feature)) return false; const props = feature.properties || {}; const isRemoved = props.removed === true || props.removed === 'True'; + const isConflicted = !isRemoved && (props.conflict === true || props.conflict === 'True'); const isService = props.highway === 'service' || props.highway === 'track'; const isUnclassified = props.highway === 'unclassified'; const showAdded = document.getElementById('showAdded').checked; + const showConflicted = document.getElementById('showConflicted').checked; const showRemoved = document.getElementById('showRemoved').checked; const hideService = document.getElementById('hideService').checked; const hideUnclassified = document.getElementById('hideUnclassified').checked; - // Check removed/added filter + // Check removed/added/conflicted filter if (isRemoved && !showRemoved) return false; - if (!isRemoved && !showAdded) return false; + if (isConflicted && !showConflicted) return false; + if (!isRemoved && !isConflicted && !showAdded) return false; // Check service/track and unclassified filters if (isService && hideService) return false; @@ -809,7 +816,10 @@ function showMultiFeaturePopup(latlng) { if (selectedFeatures.length === 1) { // Single selection - show specific status const isRemoved = selectedFeatures[0].properties && (selectedFeatures[0].properties.removed === true || selectedFeatures[0].properties.removed === 'True'); - html += `