|
|
""" |
|
|
Geocoding service for FleetMind |
|
|
Handles address validation with HERE API and smart mock fallback |
|
|
""" |
|
|
|
|
|
import os |
|
|
import requests |
|
|
import logging |
|
|
from typing import Dict, Optional |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
CITY_COORDINATES = { |
|
|
"san francisco": (37.7749, -122.4194), |
|
|
"sf": (37.7749, -122.4194), |
|
|
"new york": (40.7128, -74.0060), |
|
|
"nyc": (40.7128, -74.0060), |
|
|
"los angeles": (34.0522, -118.2437), |
|
|
"la": (34.0522, -118.2437), |
|
|
"chicago": (41.8781, -87.6298), |
|
|
"houston": (29.7604, -95.3698), |
|
|
"phoenix": (33.4484, -112.0740), |
|
|
"philadelphia": (39.9526, -75.1652), |
|
|
"san antonio": (29.4241, -98.4936), |
|
|
"san diego": (32.7157, -117.1611), |
|
|
"dallas": (32.7767, -96.7970), |
|
|
"austin": (30.2672, -97.7431), |
|
|
"seattle": (47.6062, -122.3321), |
|
|
"boston": (42.3601, -71.0589), |
|
|
"denver": (39.7392, -104.9903), |
|
|
"miami": (25.7617, -80.1918), |
|
|
"atlanta": (33.7490, -84.3880), |
|
|
"portland": (45.5152, -122.6784), |
|
|
} |
|
|
|
|
|
|
|
|
class GeocodingService: |
|
|
"""Handle address geocoding with HERE API and smart mock fallback""" |
|
|
|
|
|
def __init__(self): |
|
|
self.here_api_key = os.getenv("HERE_API_KEY", "") |
|
|
self.use_mock = not self.here_api_key or self.here_api_key.startswith("your_") |
|
|
|
|
|
if self.use_mock: |
|
|
logger.info("Geocoding: Using mock (HERE_API_KEY not configured)") |
|
|
else: |
|
|
logger.info("Geocoding: Using HERE Maps API") |
|
|
|
|
|
def geocode(self, address: str) -> Dict: |
|
|
""" |
|
|
Geocode address, using mock if API unavailable |
|
|
|
|
|
Args: |
|
|
address: Street address to geocode |
|
|
|
|
|
Returns: |
|
|
Dict with keys: lat, lng, formatted_address, confidence |
|
|
""" |
|
|
if self.use_mock: |
|
|
return self._geocode_mock(address) |
|
|
else: |
|
|
try: |
|
|
return self._geocode_here(address) |
|
|
except Exception as e: |
|
|
logger.error(f"HERE API failed: {e}, falling back to mock") |
|
|
return self._geocode_mock(address) |
|
|
|
|
|
def _geocode_here(self, address: str) -> Dict: |
|
|
"""Real HERE API geocoding""" |
|
|
url = "https://geocode.search.hereapi.com/v1/geocode" |
|
|
|
|
|
params = { |
|
|
"q": address, |
|
|
"apiKey": self.here_api_key |
|
|
} |
|
|
|
|
|
response = requests.get(url, params=params, timeout=10) |
|
|
response.raise_for_status() |
|
|
|
|
|
data = response.json() |
|
|
|
|
|
if not data.get("items"): |
|
|
|
|
|
logger.warning(f"HERE API found no results for: {address}") |
|
|
return self._geocode_mock(address) |
|
|
|
|
|
|
|
|
item = data["items"][0] |
|
|
position = item["position"] |
|
|
|
|
|
return { |
|
|
"lat": position["lat"], |
|
|
"lng": position["lng"], |
|
|
"formatted_address": item.get("address", {}).get("label", address), |
|
|
"confidence": "high (HERE API)" |
|
|
} |
|
|
|
|
|
def _geocode_mock(self, address: str) -> Dict: |
|
|
""" |
|
|
Smart mock geocoding for testing |
|
|
Tries to detect city name and use approximate coordinates |
|
|
""" |
|
|
address_lower = address.lower() |
|
|
|
|
|
|
|
|
for city, coords in CITY_COORDINATES.items(): |
|
|
if city in address_lower: |
|
|
logger.info(f"Mock geocoding detected city: {city}") |
|
|
return { |
|
|
"lat": coords[0], |
|
|
"lng": coords[1], |
|
|
"formatted_address": address, |
|
|
"confidence": f"medium (mock - {city})" |
|
|
} |
|
|
|
|
|
|
|
|
logger.info("Mock geocoding: Using default SF coordinates") |
|
|
return { |
|
|
"lat": 37.7749, |
|
|
"lng": -122.4194, |
|
|
"formatted_address": address, |
|
|
"confidence": "low (mock - default)" |
|
|
} |
|
|
|
|
|
def get_status(self) -> str: |
|
|
"""Get geocoding service status""" |
|
|
if self.use_mock: |
|
|
return "⚠️ Using mock geocoding (add HERE_API_KEY for real)" |
|
|
else: |
|
|
return "✅ HERE Maps API connected" |
|
|
|