bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
297 lines
8.9 KiB
Python
297 lines
8.9 KiB
Python
"""
|
|
Map Matcher
|
|
Match observations against OpenStreetMap and other GIS data
|
|
"""
|
|
|
|
from typing import Dict, List, Optional, Tuple
|
|
from dataclasses import dataclass
|
|
import math
|
|
|
|
|
|
@dataclass
|
|
class MapMatch:
|
|
"""Map match result"""
|
|
osm_id: str
|
|
name: str
|
|
match_type: str # street, building, poi, etc.
|
|
distance: float # meters
|
|
confidence: float
|
|
tags: Dict
|
|
|
|
|
|
class MapMatcher:
|
|
"""
|
|
Match observations against map data
|
|
|
|
Sources:
|
|
- OpenStreetMap (OSM)
|
|
- Municipal GIS data
|
|
- Property maps
|
|
- Road databases
|
|
- quiXzoom historical observations
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.osm_data = {} # In production, load from OSM API or local database
|
|
self.historical_observations = []
|
|
|
|
def match_location(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
evidence: Dict,
|
|
radius: float = 100.0 # meters
|
|
) -> List[MapMatch]:
|
|
"""
|
|
Match location against map data
|
|
|
|
Returns list of potential matches with confidence scores
|
|
"""
|
|
matches = []
|
|
|
|
# 1. Match against OSM streets
|
|
street_matches = self._match_streets(lat, lng, evidence, radius)
|
|
matches.extend(street_matches)
|
|
|
|
# 2. Match against OSM buildings
|
|
building_matches = self._match_buildings(lat, lng, evidence, radius)
|
|
matches.extend(building_matches)
|
|
|
|
# 3. Match against OSM POIs
|
|
poi_matches = self._match_pois(lat, lng, evidence, radius)
|
|
matches.extend(poi_matches)
|
|
|
|
# 4. Match against historical observations
|
|
historical_matches = self._match_historical(lat, lng, evidence, radius)
|
|
matches.extend(historical_matches)
|
|
|
|
# Sort by confidence
|
|
matches.sort(key=lambda x: x.confidence, reverse=True)
|
|
|
|
return matches
|
|
|
|
def _match_streets(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
evidence: Dict,
|
|
radius: float
|
|
) -> List[MapMatch]:
|
|
"""Match against OSM streets"""
|
|
matches = []
|
|
|
|
# In production, query OSM API or local database
|
|
# For demo, simulate matches
|
|
|
|
# Check if street names in evidence match known streets
|
|
text_evidence = evidence.get("text_detections", [])
|
|
|
|
known_streets = {
|
|
"Sukhumvit Road": {
|
|
"osm_id": "way_12345",
|
|
"lat": 13.7563,
|
|
"lng": 100.5018,
|
|
"tags": {"highway": "primary", "name": "Sukhumvit Road"}
|
|
},
|
|
"Silom Road": {
|
|
"osm_id": "way_67890",
|
|
"lat": 13.7285,
|
|
"lng": 100.5293,
|
|
"tags": {"highway": "primary", "name": "Silom Road"}
|
|
}
|
|
}
|
|
|
|
for text in text_evidence:
|
|
street_name = text.get("text", "")
|
|
if street_name in known_streets:
|
|
street = known_streets[street_name]
|
|
distance = self._haversine_distance(lat, lng, street["lat"], street["lng"])
|
|
|
|
if distance <= radius:
|
|
confidence = self._calculate_match_confidence(
|
|
distance, radius, text.get("confidence", 0.5)
|
|
)
|
|
|
|
matches.append(MapMatch(
|
|
osm_id=street["osm_id"],
|
|
name=street_name,
|
|
match_type="street",
|
|
distance=distance,
|
|
confidence=confidence,
|
|
tags=street["tags"]
|
|
))
|
|
|
|
return matches
|
|
|
|
def _match_buildings(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
evidence: Dict,
|
|
radius: float
|
|
) -> List[MapMatch]:
|
|
"""Match against OSM buildings"""
|
|
matches = []
|
|
|
|
# In production, query OSM building data
|
|
# For demo, return empty
|
|
|
|
return matches
|
|
|
|
def _match_pois(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
evidence: Dict,
|
|
radius: float
|
|
) -> List[MapMatch]:
|
|
"""Match against OSM POIs"""
|
|
matches = []
|
|
|
|
# Check for business names in evidence
|
|
text_evidence = evidence.get("text_detections", [])
|
|
|
|
known_pois = {
|
|
"Sainokuni": {
|
|
"osm_id": "node_11111",
|
|
"lat": 13.7565,
|
|
"lng": 100.5020,
|
|
"tags": {"amenity": "restaurant", "name": "Sainokuni", "cuisine": "japanese"}
|
|
}
|
|
}
|
|
|
|
for text in text_evidence:
|
|
business_name = text.get("text", "")
|
|
for poi_name, poi in known_pois.items():
|
|
if poi_name.lower() in business_name.lower():
|
|
distance = self._haversine_distance(lat, lng, poi["lat"], poi["lng"])
|
|
|
|
if distance <= radius:
|
|
confidence = self._calculate_match_confidence(
|
|
distance, radius, text.get("confidence", 0.5)
|
|
)
|
|
|
|
matches.append(MapMatch(
|
|
osm_id=poi["osm_id"],
|
|
name=poi_name,
|
|
match_type="poi",
|
|
distance=distance,
|
|
confidence=confidence,
|
|
tags=poi["tags"]
|
|
))
|
|
|
|
return matches
|
|
|
|
def _match_historical(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
evidence: Dict,
|
|
radius: float
|
|
) -> List[MapMatch]:
|
|
"""Match against historical observations"""
|
|
matches = []
|
|
|
|
# In production, query database of previous observations
|
|
# For demo, simulate one historical match
|
|
|
|
historical = {
|
|
"obs_001": {
|
|
"lat": 13.7563,
|
|
"lng": 100.5018,
|
|
"timestamp": "2026-03-15T10:00:00Z",
|
|
"visual_objects": ["street_light", "building"],
|
|
"embedding_similarity": 0.92
|
|
}
|
|
}
|
|
|
|
for obs_id, obs in historical.items():
|
|
distance = self._haversine_distance(lat, lng, obs["lat"], obs["lng"])
|
|
|
|
if distance <= radius:
|
|
confidence = obs.get("embedding_similarity", 0.5) * (1 - distance / radius)
|
|
|
|
matches.append(MapMatch(
|
|
osm_id=obs_id,
|
|
name=f"Historical observation {obs_id}",
|
|
match_type="historical",
|
|
distance=distance,
|
|
confidence=confidence,
|
|
tags={"timestamp": obs["timestamp"]}
|
|
))
|
|
|
|
return matches
|
|
|
|
def _calculate_match_confidence(
|
|
self,
|
|
distance: float,
|
|
radius: float,
|
|
evidence_confidence: float
|
|
) -> float:
|
|
"""Calculate confidence based on distance and evidence quality"""
|
|
# Distance factor: closer = higher confidence
|
|
distance_factor = 1 - (distance / radius)
|
|
|
|
# Combine with evidence confidence
|
|
confidence = distance_factor * evidence_confidence
|
|
|
|
return max(0.0, min(1.0, confidence))
|
|
|
|
def _haversine_distance(self, lat1, lng1, lat2, lng2) -> float:
|
|
"""Calculate distance between two coordinates in meters"""
|
|
R = 6371000 # Earth radius in meters
|
|
|
|
phi1 = math.radians(lat1)
|
|
phi2 = math.radians(lat2)
|
|
delta_phi = math.radians(lat2 - lat1)
|
|
delta_lambda = math.radians(lng2 - lng1)
|
|
|
|
a = math.sin(delta_phi / 2) ** 2 + \
|
|
math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2) ** 2
|
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
|
|
return R * c
|
|
|
|
|
|
# Example usage
|
|
def test_map_matching():
|
|
"""Test map matching"""
|
|
print("=== Testing Map Matching ===\n")
|
|
|
|
matcher = MapMatcher()
|
|
|
|
# Simulate evidence from Bangkok image
|
|
evidence = {
|
|
"text_detections": [
|
|
{"text": "Sukhumvit Road", "confidence": 0.92},
|
|
{"text": "Sainokuni", "confidence": 0.85}
|
|
],
|
|
"visual_objects": [
|
|
{"label": "street_light", "confidence": 0.85},
|
|
{"label": "building", "confidence": 0.92}
|
|
]
|
|
}
|
|
|
|
# Match location
|
|
matches = matcher.match_location(
|
|
lat=13.7563,
|
|
lng=100.5018,
|
|
evidence=evidence,
|
|
radius=100
|
|
)
|
|
|
|
print(f"Found {len(matches)} matches:")
|
|
for match in matches:
|
|
print(f" - {match.name} ({match.match_type})")
|
|
print(f" Distance: {match.distance:.1f}m")
|
|
print(f" Confidence: {match.confidence:.2f}")
|
|
print(f" OSM ID: {match.osm_id}")
|
|
print(f" Tags: {match.tags}")
|
|
print()
|
|
|
|
return matches
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_map_matching()
|