diff --git a/app.py b/app.py index 6d126a2..83d1264 100644 --- a/app.py +++ b/app.py @@ -163,36 +163,23 @@ def api_delete_trip(trip_id): return jsonify(status="deleted"), 200 import functools -import urllib.request +import reverse_geocoder as rg @functools.lru_cache(maxsize=512) def _geocode_cached(lat4, lon4): - """Try Nominatim first (street-level), fall back to reverse_geocoder (city), - then raw coordinates. Always returns a non-empty label string.""" + """Reverse-geocode a coordinate to city/province using the offline + reverse_geocoder library. Always returns a non-empty label string.""" lat = lat4 / 10000.0 lon = lon4 / 10000.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 + results = rg.search([(lat, lon)], mode=1, verbose=False) + if results: + r = results[0] + label = ", ".join(p for p in [r.get("name", ""), r.get("admin1", "")] 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] - label = ", ".join(p for p in [r.get("name", ""), r.get("admin1", "")] if p) - if label: - return label - # Last resort — raw coordinates so the frontend never gets an empty label return f"{lat:.5f}, {lon:.5f}" @app.route('/api/geocode') diff --git a/server/app.py b/server/app.py index ba3d9c9..e4b73d4 100644 --- a/server/app.py +++ b/server/app.py @@ -2,7 +2,6 @@ uploaded by the Pi, storing them in Postgres + on-disk file storage. Also serves the remote web portal for browsing and managing the configured car. """ -import concurrent.futures import functools import hashlib import os @@ -21,45 +20,28 @@ from modules.user_auth import login_manager, User, check_password, require_login from modules import device_config, device_registry # ── Geocode cache ────────────────────────────────────────────────────────── -try: - import reverse_geocoder as _rg - _RG_AVAILABLE = True -except ImportError: - _RG_AVAILABLE = False +# reverse_geocoder is an offline Python library that maps lat/lon → +# city/province using a precomputed gazetteer (~40MB). Instant, no network, +# always available — a much better fit for trip scrubbing than Nominatim +# which is slow, rate-limited, and often unreachable from Docker/Cloudflare. +import reverse_geocoder as _rg @functools.lru_cache(maxsize=512) def _geocode_cached(lat4, lon4): - """Try Nominatim first (street-level), fall back to reverse_geocoder (city), - then raw coordinates. Always returns a non-empty label string — never None.""" + """Reverse-geocode a coordinate to city/province using the offline + reverse_geocoder library. Always returns a non-empty label string.""" lat = lat4 / 10000.0 lon = lon4 / 10000.0 try: - import urllib.request, urllib.error, json as _json - url = ( - f"https://nominatim.openstreetmap.org/reverse" - f"?lat={lat}&lon={lon}&format=jsonv2&accept-language=en" - ) - req = urllib.request.Request(url, headers={"User-Agent": "yaris-obd-server/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 - if _RG_AVAILABLE: results = _rg.search([(lat, lon)], mode=1, verbose=False) if results: r = results[0] label = ", ".join(p for p in [r.get("name", ""), r.get("admin1", "")] if p) if label: return label - # Last resort — raw coordinates so the frontend never gets an empty label + except Exception: + pass return f"{lat:.5f}, {lon:.5f}" @@ -629,16 +611,14 @@ def geocode(): def geocode_batch(): """Batch geocode — returns {key: label} for all unique coords in one request. - Parallelised with ThreadPoolExecutor (max_workers=4) so Nominatim HTTP - calls and reverse_geocoder lookups run concurrently instead of sequentially. - _geocode_cached's lru_cache absorbs duplicate coords within the batch.""" + Uses the offline reverse_geocoder library (instant, no network). Deduplicates + coords before lookup; lru_cache absorbs any remaining duplicates.""" body = request.get_json(silent=True) or {} coords = body.get("coords") if not isinstance(coords, list) or not coords: return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400 - # Deduplicate — same (lat4,lon4) would hit lru_cache anyway, but this - # avoids submitting redundant futures. + # Deduplicate seen = set() items = [] for item in coords: @@ -654,19 +634,8 @@ def geocode_batch(): items.append((f"{lat:.3f},{lon:.3f}", lat4, lon4)) result = {} - if not items: - return jsonify(result) - - with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex: - futures = {ex.submit(_geocode_cached, lat4, lon4): key for key, lat4, lon4 in items} - for future in concurrent.futures.as_completed(futures): - key = futures[future] - try: - result[key] = future.result(timeout=8) - except Exception: - # Last resort — raw coords if even the fallback failed - parts = key.split(",") - result[key] = f"{float(parts[0]):.5f}, {float(parts[1]):.5f}" + for key, lat4, lon4 in items: + result[key] = _geocode_cached(lat4, lon4) return jsonify(result)