| """Address geocoding via the Google Geocoding API. |
| |
| Used by the AI intake flow: when Claude extracts a claim whose text has |
| an address but no explicit coordinates, this resolves the address to |
| lat/lon so the claim lands on the map at its real location instead of |
| the region-center placeholder. |
| |
| Key-gated: uses GOOGLE_MAPS_API_KEY (the same key as the Routes API; |
| enable 'Geocoding API' on the Google Cloud project). Without a key, |
| callers fall back to the region-center placeholder. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| import urllib.parse |
| import urllib.request |
|
|
| ENDPOINT = "https://maps.googleapis.com/maps/api/geocode/json" |
|
|
|
|
| class GeocodeError(RuntimeError): |
| pass |
|
|
|
|
| def geocode(address: str, api_key: str | None = None, |
| region_hint: str = "us") -> tuple[float, float]: |
| """Resolve an address to (lat, lon). Raises GeocodeError on failure.""" |
| api_key = api_key or os.environ.get("GOOGLE_MAPS_API_KEY") |
| if not api_key: |
| raise GeocodeError("GOOGLE_MAPS_API_KEY not set") |
| params = urllib.parse.urlencode({ |
| "address": address, "key": api_key, "region": region_hint, |
| }) |
| try: |
| with urllib.request.urlopen(f"{ENDPOINT}?{params}", |
| timeout=15) as resp: |
| data = json.loads(resp.read()) |
| except OSError as e: |
| raise GeocodeError(f"Geocoding request failed: {e}") from e |
| if data.get("status") != "OK" or not data.get("results"): |
| raise GeocodeError(f"Geocoding returned {data.get('status')} " |
| f"for {address!r}") |
| loc = data["results"][0]["geometry"]["location"] |
| return float(loc["lat"]), float(loc["lng"]) |
|
|