diff --git a/backend/app/services/site_finder/poi_loader.py b/backend/app/services/site_finder/poi_loader.py index e70e0053..821702dd 100644 --- a/backend/app/services/site_finder/poi_loader.py +++ b/backend/app/services/site_finder/poi_loader.py @@ -42,17 +42,20 @@ OSM_CATEGORIES: dict[tuple[str, str], str] = { } -def _build_overpass_query() -> str: - """Строит Overpass QL запрос для всех POI типов в bbox ЕКБ.""" +def _build_overpass_query_single(key: str, value: str) -> str: + """Запрос для одной пары tag → нормированной категории. + + Раньше делали один большой запрос на все 14 категорий — Overpass возвращал + 504 Gateway Timeout (запрос слишком тяжёлый). Сплит на per-category даёт + 14 быстрых запросов вместо одного 60+ секундного. + """ south, west, north, east = EKB_BBOX bbox = f"({south},{west},{north},{east})" - # out body даёт теги + meta для timestamp последней правки - queries: list[str] = [] - for (k, v), _cat in OSM_CATEGORIES.items(): - queries.append(f'node["{k}"="{v}"]{bbox};') - queries.append(f'way["{k}"="{v}"]{bbox};') - inner = "\n".join(queries) - return f"[out:json][timeout:60];(\n{inner}\n);out center meta;" + return ( + f"[out:json][timeout:30];" + f'(node["{key}"="{value}"]{bbox};way["{key}"="{value}"]{bbox};);' + f"out center meta;" + ) def _classify(tags: dict[str, str]) -> str | None: @@ -64,24 +67,37 @@ def _classify(tags: dict[str, str]) -> str | None: async def fetch_overpass() -> list[dict]: - """Запросить Overpass API, вернуть список raw OSM elements. + """Запросить Overpass API per category, вернуть combined список elements. - NB: Overpass блокирует default `python-httpx/*` User-Agent (отдаёт 406). - Передаём явный UA с контактом проекта — это требование Overpass usage - policy (https://operations.osmfoundation.org/policies/api/). + Делаем 14 отдельных запросов вместо одного гигантского — большой запрос + отдаёт 504 Gateway Timeout. Между запросами sleep 1с (Overpass usage + policy: max 2 concurrent, лучше 1 req/s). + + Overpass блокирует default `python-httpx/*` User-Agent (406) — поэтому + явный UA с контактом проекта. """ - query = _build_overpass_query() - logger.info("Overpass: отправляем запрос (%d символов)", len(query)) headers = { "User-Agent": "GenDesign-SiteFinder/1.0 (+https://gendsgn.ru)", "Accept": "application/json", } - async with httpx.AsyncClient(timeout=120, headers=headers) as client: - r = await client.post(OVERPASS_URL, data={"data": query}) - r.raise_for_status() - elements: list[dict] = r.json().get("elements", []) - logger.info("Overpass: получено %d элементов", len(elements)) - return elements + all_elements: list[dict] = [] + async with httpx.AsyncClient(timeout=60, headers=headers) as client: + for (key, value), category in OSM_CATEGORIES.items(): + query = _build_overpass_query_single(key, value) + try: + r = await client.post(OVERPASS_URL, data={"data": query}) + r.raise_for_status() + elements: list[dict] = r.json().get("elements", []) + logger.info("Overpass: %s=%s (%s) → %d", key, value, category, len(elements)) + all_elements.extend(elements) + except Exception as e: + # Не падаем на одной категории — логируем и продолжаем + logger.warning("Overpass failed for %s=%s: %s", key, value, e) + await asyncio.sleep(1.0) + logger.info( + "Overpass: total %d elements across %d categories", len(all_elements), len(OSM_CATEGORIES) + ) + return all_elements def sync_poi_to_db() -> dict[str, int]: