diff --git a/lib/geocode.py b/lib/geocode.py index 22acdf9..312cca7 100644 --- a/lib/geocode.py +++ b/lib/geocode.py @@ -334,21 +334,20 @@ def _retrieve_photon_structured(parsed, limit=10): return _parse_photon_features(data.get('features', []), 'photon') -def _retrieve_photon_freetext(query, limit=10): +def _retrieve_photon_freetext(query, limit=10, lat=None, lon=None, zoom=None): """Query Photon /api for free-text search with location bias.""" try: params = { 'q': query, 'limit': limit, - 'lat': GEOCODE_BIAS_LAT, - 'lon': GEOCODE_BIAS_LON, - 'zoom': GEOCODE_BIAS_ZOOM, + 'lat': lat if lat is not None else GEOCODE_BIAS_LAT, + 'lon': lon if lon is not None else GEOCODE_BIAS_LON, + 'zoom': int(zoom) if zoom is not None else GEOCODE_BIAS_ZOOM, } resp = requests.get(f"{PHOTON_URL}/api", params=params, timeout=5) resp.raise_for_status() data = resp.json() except Exception as e: - logger.debug("Photon /api failed: %s", e) return [] return _parse_photon_features(data.get('features', []), 'photon') @@ -663,7 +662,7 @@ def _annotate_with_address_book(results): # PUBLIC API # ═══════════════════════════════════════════════════════════════════ -def geocode(query, limit=10): +def geocode(query, limit=10, lat=None, lon=None, zoom=None): """ Structured geocoding with multi-source retrieval and reranking. @@ -731,7 +730,7 @@ def geocode(query, limit=10): # Parallel: Netsyms (structured) + Photon (freetext with expanded query) netsyms_results = _retrieve_netsyms(parsed, limit=limit) photon_results = _retrieve_photon_freetext( - parsed.get('expanded_query', q), limit=limit + parsed.get('expanded_query', q), limit=limit, lat=lat, lon=lon, zoom=zoom ) # Also try Photon /structured for addresses photon_struct = _retrieve_photon_structured(parsed, limit=5) @@ -739,11 +738,11 @@ def geocode(query, limit=10): elif intent == 'POSTCODE': netsyms_results = _retrieve_netsyms(parsed, limit=limit) - photon_results = _retrieve_photon_freetext(q, limit=limit) + photon_results = _retrieve_photon_freetext(q, limit=limit, lat=lat, lon=lon, zoom=zoom) candidates = netsyms_results + photon_results elif intent in ('LOCALITY', 'POI', 'UNKNOWN'): - candidates = _retrieve_photon_freetext(q, limit=limit) + candidates = _retrieve_photon_freetext(q, limit=limit, lat=lat, lon=lon, zoom=zoom) # ── Deduplicate by (lat, lon) proximity ── deduped = [] diff --git a/lib/nav_tools.py b/lib/nav_tools.py index 2f91616..d4bb1f7 100644 --- a/lib/nav_tools.py +++ b/lib/nav_tools.py @@ -50,10 +50,10 @@ def _haversine_m(lat1, lon1, lat2, lon2): return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) -def geocode(query: str, limit: int = 10): +def geocode(query: str, limit: int = 10, lat=None, lon=None, zoom=None): """Delegate to the structured geocode module. See lib/geocode.py.""" from . import geocode as geocode_mod - return geocode_mod.geocode(query, limit=limit) + return geocode_mod.geocode(query, limit=limit, lat=lat, lon=lon, zoom=zoom) def _geocode(query: str): diff --git a/lib/netsyms_api.py b/lib/netsyms_api.py index 92c8b6e..4a0847f 100644 --- a/lib/netsyms_api.py +++ b/lib/netsyms_api.py @@ -35,6 +35,19 @@ def api_netsyms_health(): return jsonify(netsyms.health()) + +def _safe_float(val, lo, hi): + """Parse val as float; return None if missing, non-numeric, or out of [lo, hi].""" + if val is None: + return None + try: + f = float(val) + if lo <= f <= hi: + return f + except (ValueError, TypeError): + pass + return None + @geocode_bp.route('/api/geocode') def api_geocode(): """ @@ -58,7 +71,12 @@ def api_geocode(): except (ValueError, TypeError): limit = 10 - result = nav_tools.geocode(q, limit=limit) + # Viewport bias parameters (optional) + lat = _safe_float(request.args.get("lat"), -90, 90) + lon = _safe_float(request.args.get("lon"), -180, 180) + zoom = _safe_float(request.args.get("zoom"), 0, 22) + + result = nav_tools.geocode(q, limit=limit, lat=lat, lon=lon, zoom=zoom) return jsonify(result)