feat(geocode): street-level via Nominatim when online, city fallback offline

- Try Nominatim first (road + city), 4s timeout
- Fall back to reverse_geocoder (city + region) if no internet
- LRU cache (512 entries, ~111m precision) — repeated lookups are instant
- No external calls from browser, all via /api/georecode on Pi

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 03:18:36 -04:00
co-authored by Copilot
parent e96c6a16bb
commit c188fb9d14
+31 -8
View File
@@ -89,6 +89,35 @@ def trip_route(trip_id):
return jsonify(error=f"Trip not found: {trip_id}"), 404 return jsonify(error=f"Trip not found: {trip_id}"), 404
return jsonify(trip) return jsonify(trip)
import functools
import urllib.request
@functools.lru_cache(maxsize=512)
def _geocode_cached(lat3, lon3):
"""Try Nominatim first (street-level), fall back to reverse_geocoder (city)."""
lat = lat3 / 1000.0
lon = lon3 / 1000.0
try:
url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}&accept-language=en"
req = urllib.request.Request(url, headers={"User-Agent": "yaris-obd/1.0"})
with urllib.request.urlopen(req, timeout=4) as resp:
data = json.loads(resp.read())
a = data.get("address", {})
road = a.get("road") or a.get("hamlet") or a.get("suburb") or ""
city = a.get("city") or a.get("town") or a.get("village") or a.get("municipality") or ""
label = ", ".join(p for p in [road, city] if p)
if label:
return label
except Exception:
pass
# Offline fallback
import reverse_geocoder as rg
results = rg.search([(lat, lon)], mode=1, verbose=False)
if results:
r = results[0]
return ", ".join(p for p in [r.get("name", ""), r.get("admin1", "")] if p)
return f"{lat:.5f}, {lon:.5f}"
@app.route('/api/geocode') @app.route('/api/geocode')
def geocode_route(): def geocode_route():
try: try:
@@ -96,14 +125,8 @@ def geocode_route():
lon = float(request.args['lon']) lon = float(request.args['lon'])
except (KeyError, ValueError): except (KeyError, ValueError):
return jsonify(error="lat and lon required"), 400 return jsonify(error="lat and lon required"), 400
import reverse_geocoder as rg # Round to 3 decimals (~111m precision) for cache key
results = rg.search([(lat, lon)], mode=1, verbose=False) label = _geocode_cached(round(lat, 3) * 1000 // 1, round(lon, 3) * 1000 // 1)
if results:
r = results[0]
parts = [r.get('name', ''), r.get('admin1', '')]
label = ', '.join(p for p in parts if p)
else:
label = f"{lat:.5f}, {lon:.5f}"
return jsonify(label=label) return jsonify(label=label)
@app.route('/api/dash/devices') @app.route('/api/dash/devices')