Files
2026-08-21 15:04:46 +02:00

307 lines
6.9 KiB
Python

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
)