from flask import Flask, render_template, request, jsonify import requests import time app = Flask(__name__) NOMINATIM_SEARCH_URL = "https://nominatim.openstreetmap.org/search" NOMINATIM_REVERSE_URL = "https://nominatim.openstreetmap.org/reverse" # Nominatim asks clients to identify themselves. HEADERS = { "User-Agent": "LibreMapViewer/1.0 (local Flask application)" } # Very simple rate limiting so we don't hammer Nominatim. last_request_time = 0 MIN_REQUEST_INTERVAL = 1.0 def classify_place(item): """ Classify a Nominatim result into one of: - "area" : countries, states, cities, towns, villages, suburbs, neighbourhoods, water bodies, natural features, etc. - "poi" : shops, restaurants, hotels, buildings, attractions, ... - "address" : street / house-number level results - "other" : anything else The client uses this to pick an appropriate zoom level when the user selects a result. """ category = (item.get("category") or "").lower() place_type = (item.get("type") or "").lower() address = item.get("address") or {} namedetails = item.get("namedetails") or {} # Administrative boundaries are always areas. if place_type.endswith("_boundary"): return "area" # Place categories (city, town, village, suburb, ...) are areas. if category == "place": return "area" # Boundaries are areas. if category == "boundary": return "area" # Natural & water features are treated as areas (they span regions). if category in ("natural", "water", "waterway"): return "area" # A real street/house-number result is an address. if ( address.get("house_number") or address.get("road") ): if ( namedetails.get("name") or namedetails.get("addr:housenumber") or address.get("house_number") ): return "address" # Known POI categories. poi_categories = { "amenity", "shop", "tourism", "leisure", "office", "craft", "building", "historic", "industrial", "public_transport", } if category in poi_categories: return "poi" return "other" def nominatim_request(url, params): """ Make a request to Nominatim while respecting a small delay between requests. """ global last_request_time elapsed = time.time() - last_request_time if elapsed < MIN_REQUEST_INTERVAL: time.sleep(MIN_REQUEST_INTERVAL - elapsed) response = requests.get( url, params=params, headers=HEADERS, timeout=15 ) last_request_time = time.time() response.raise_for_status() return response.json() @app.route("/") def index(): return render_template("index.html") @app.route("/search") def search(): query = request.args.get("q", "").strip() if not query: return jsonify({ "error": "Please enter a location" }), 400 params = { "q": query, "format": "jsonv2", "limit": 20, "addressdetails": 1, "extratags": 1, "namedetails": 1, "dedupe": 1, } # Restrict results to the area currently visible on the map. # Expected format: "left,top,right,bottom" (lon,lat pairs). viewbox = request.args.get("viewbox", "").strip() if viewbox: parts = [p.strip() for p in viewbox.split(",")] if len(parts) == 4: try: left, top, right, bottom = ( float(p) for p in parts ) params["viewbox"] = "{},{},{},{}".format( left, top, right, bottom ) params["bounded"] = 1 except ValueError: pass try: results = nominatim_request( NOMINATIM_SEARCH_URL, params ) except requests.RequestException as e: return jsonify({ "error": f"Search failed: {e}" }), 500 normalized = [] for item in results: normalized.append({ "place_id": item.get("place_id"), "osm_type": item.get("osm_type"), "osm_id": item.get("osm_id"), "display_name": item.get("display_name", ""), "lat": float(item["lat"]), "lon": float(item["lon"]), "type": item.get("type"), "category": item.get("category"), "kind": classify_place(item), "address": item.get("address", {}), "extratags": item.get("extratags", {}), "namedetails": item.get("namedetails", {}), "importance": item.get("importance", 0), "boundingbox": item.get("boundingbox"), }) return jsonify(normalized) @app.route("/geocode") def geocode(): lat = request.args.get("lat") lon = request.args.get("lon") if not lat or not lon: return jsonify({ "error": "Missing latitude or longitude" }), 400 try: lat = float(lat) lon = float(lon) except ValueError: return jsonify({ "error": "Invalid latitude or longitude" }), 400 params = { "lat": lat, "lon": lon, "format": "jsonv2", "zoom": 18, "addressdetails": 1, "extratags": 1, "namedetails": 1, } try: result = nominatim_request( NOMINATIM_REVERSE_URL, params ) except requests.RequestException as e: return jsonify({ "error": f"Reverse geocoding failed: {e}" }), 500 if not result: return jsonify({ "display_name": None }) return jsonify({ "place_id": result.get("place_id"), "osm_type": result.get("osm_type"), "osm_id": result.get("osm_id"), "display_name": result.get("display_name", ""), "lat": float(result.get("lat", lat)), "lon": float(result.get("lon", lon)), "type": result.get("type"), "category": result.get("category"), "address": result.get("address", {}), "extratags": result.get("extratags", {}), "namedetails": result.get("namedetails", {}), }) if __name__ == "__main__": app.run( port=5000, debug=True )