Files
fn_registry/python/functions/infra/overpass_nearby_pois.py
T
egutierrez dede5e00af feat: add Python core and infra functions — PWA, geocoding, POI matching
Nuevas funciones Python: build_guide_prompt, generate_pwa_manifest,
generate_service_worker, match_pois_to_interests (core), nominatim_reverse_geocode,
ollama_chat, overpass_nearby_pois (infra). Incluye tests unitarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:19 +02:00

139 lines
4.5 KiB
Python

"""Consulta la Overpass API para obtener POIs cercanos a una coordenada."""
import json
import urllib.error
import urllib.parse
import urllib.request
# Mapeo de categoría legible → (key OSM, value OSM)
# None como value significa wildcard (key=*)
_CATEGORY_TAGS: dict[str, tuple[str, str | None]] = {
"restaurant": ("amenity", "restaurant"),
"cafe": ("amenity", "cafe"),
"bar": ("amenity", "bar"),
"museum": ("tourism", "museum"),
"monument": ("historic", "monument"),
"park": ("leisure", "park"),
"library": ("amenity", "library"),
"theatre": ("amenity", "theatre"),
"cinema": ("amenity", "cinema"),
"gallery": ("tourism", "gallery"),
"historic": ("historic", None),
"tourism": ("tourism", None),
"shop": ("shop", None),
"hotel": ("tourism", "hotel"),
"pharmacy": ("amenity", "pharmacy"),
"hospital": ("amenity", "hospital"),
}
_DEFAULT_CATEGORIES = [
"restaurant", "cafe", "bar", "museum", "monument", "park",
"library", "theatre", "cinema", "gallery", "historic", "tourism", "shop",
]
_OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter"
def _build_query(lat: float, lon: float, radius_m: int, categories: list[str]) -> str:
"""Construye una query Overpass QL con union de nodos."""
lines = ["[out:json][timeout:10];", "("]
for cat in categories:
if cat not in _CATEGORY_TAGS:
continue
key, value = _CATEGORY_TAGS[cat]
if value is None:
lines.append(f' node["{key}"](around:{radius_m},{lat},{lon});')
else:
lines.append(f' node["{key}"="{value}"](around:{radius_m},{lat},{lon});')
lines.append(");")
lines.append("out body;")
return "\n".join(lines)
def _extract_category(tags: dict) -> str:
"""Determina la categoría legible a partir de los tags OSM del nodo."""
for cat, (key, value) in _CATEGORY_TAGS.items():
if key not in tags:
continue
if value is None or tags[key] == value:
return cat
return "unknown"
def overpass_nearby_pois(
lat: float,
lon: float,
radius_m: int = 500,
categories: list[str] | None = None,
) -> list[dict]:
"""Obtiene POIs cercanos a una coordenada usando la Overpass API.
Args:
lat: latitud del punto central.
lon: longitud del punto central.
radius_m: radio de busqueda en metros (defecto 500).
categories: lista de categorias a buscar. Si es None, usa todas las
categorias comunes definidas internamente.
Returns:
lista de dicts con id, lat, lon, name, category y tags de cada POI.
Raises:
RuntimeError: si la request falla por error HTTP, timeout u otro error de red.
"""
active_categories = categories if categories is not None else _DEFAULT_CATEGORIES
query = _build_query(lat, lon, radius_m, active_categories)
encoded_query = urllib.parse.quote(query)
form_body = f"data={encoded_query}".encode("utf-8")
req = urllib.request.Request(
_OVERPASS_ENDPOINT,
data=form_body,
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
try:
with urllib.request.urlopen(req, timeout=10) as response:
raw = response.read()
except urllib.error.HTTPError as e:
body_preview = e.read(200).decode("utf-8", errors="replace")
raise RuntimeError(
f"Overpass API HTTP error {e.code} for ({lat},{lon}) r={radius_m}m: {body_preview}"
) from e
except urllib.error.URLError as e:
raise RuntimeError(
f"Overpass API request failed for ({lat},{lon}) r={radius_m}m: {e.reason}"
) from e
try:
result = json.loads(raw)
except json.JSONDecodeError as e:
raise RuntimeError(
f"Overpass API returned invalid JSON for ({lat},{lon}): {e}"
) from e
pois: list[dict] = []
for element in result.get("elements", []):
if element.get("type") != "node":
continue
tags = element.get("tags", {})
category = _extract_category(tags)
# Nombre: tag "name", o valor del tag de categoría, o "Unknown"
if "name" in tags:
name = tags["name"]
else:
key = _CATEGORY_TAGS.get(category, (None, None))[0]
name = tags.get(key, "Unknown") if key else "Unknown"
pois.append({
"id": element["id"],
"lat": element["lat"],
"lon": element["lon"],
"name": name,
"category": category,
"tags": tags,
})
return pois