From c188fb9d1461c7a2c5bf6ab83d69ac056294ce40 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Mon, 10 Aug 2026 03:18:36 -0400 Subject: [PATCH] feat(geocode): street-level via Nominatim when online, city fallback offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 <223556219+Copilot@users.noreply.github.com> --- app.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/app.py b/app.py index 165d4f0..5ad0954 100644 --- a/app.py +++ b/app.py @@ -89,6 +89,35 @@ def trip_route(trip_id): return jsonify(error=f"Trip not found: {trip_id}"), 404 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') def geocode_route(): try: @@ -96,14 +125,8 @@ def geocode_route(): lon = float(request.args['lon']) except (KeyError, ValueError): return jsonify(error="lat and lon required"), 400 - import reverse_geocoder as rg - results = rg.search([(lat, lon)], mode=1, verbose=False) - 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}" + # Round to 3 decimals (~111m precision) for cache key + label = _geocode_cached(round(lat, 3) * 1000 // 1, round(lon, 3) * 1000 // 1) return jsonify(label=label) @app.route('/api/dash/devices')