"""ОКН-lookup для analyze_parcel (#1066). Читает ``okn_objects`` (м.141) и отдаёт объекты культурного наследия в заданном радиусе от участка — [{source_id, category, distance_m}]. ST_DWithin по geography (метрическое расстояние в метрах) через GIST-индекс idx_okn_objects_geom. Участок передаётся как WKT в EPSG:4326. Graceful: если таблица ``okn_objects`` ещё не задеплоена (pre-migration) или БД-ошибка — возвращает пустой список (analyze не падает). Wiring в analyze — следующий PR (analyze-wiring, follow-up #1066). """ from __future__ import annotations import logging from typing import Any from sqlalchemy import text from sqlalchemy.exc import OperationalError, ProgrammingError from sqlalchemy.orm import Session logger = logging.getLogger(__name__) # Пространственный запрос: ST_DWithin по geography даёт расстояние в метрах. # CAST psycopg v3 (никогда :param::type — vault Pattern_CAST_AS_Type). _OKN_DWITHIN_SQL = text(""" SELECT source_id, category, ROUND( ST_Distance( geom::geography, ST_GeomFromText(CAST(:parcel_wkt AS text), 4326)::geography ) )::integer AS distance_m FROM okn_objects WHERE ST_DWithin( geom::geography, ST_GeomFromText(CAST(:parcel_wkt AS text), 4326)::geography, CAST(:radius_m AS double precision) ) ORDER BY distance_m ASC """) def parcel_okn_objects( db: Session, parcel_wkt: str | None, radius_m: float = 100.0, ) -> list[dict[str, Any]]: """ОКН-объекты в радиусе radius_m от участка. Args: db: SQLAlchemy-сессия. parcel_wkt: геометрия участка в WKT (EPSG:4326). None → пустой список. radius_m: радиус поиска в метрах (по умолчанию 100 м). Returns: Список dict [{source_id, category, distance_m}], отсортированный по расстоянию ASC. Пустой список при отсутствии участка / таблицы / объектов в радиусе (analyze не падает). """ if not parcel_wkt: return [] try: rows = db.execute( _OKN_DWITHIN_SQL, {"parcel_wkt": parcel_wkt, "radius_m": radius_m}, ).fetchall() except (OperationalError, ProgrammingError) as exc: logger.warning("parcel_okn_objects: okn_objects недоступна, skip: %s", exc) return [] except Exception as exc: logger.warning("parcel_okn_objects: ошибка запроса okn_objects: %s", exc) return [] result: list[dict[str, Any]] = [] for row in rows: result.append( { "source_id": row[0], "category": row[1], "distance_m": row[2], } ) return result __all__ = ["parcel_okn_objects"]