diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index bd423b5e..58f27692 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -2933,18 +2933,25 @@ def get_isochrones( detail=f"times_min значения вне диапазона 1-60: {invalid_times}", ) - # Получить координаты центроида из доступных геометрий участка + # Получить координаты центроида из доступных геометрий участка. + # `geom IS NOT NULL` во ВСЕХ ветках UNION (#1201): cad_parcels_geom — VIEW + # над cad_parcels с nullable geom, 964 prod-строки. Без фильтра NULL-строка + # ранней ветки скрывает валидную из поздней + ST_Centroid(NULL) → NULL → 500. + # Зеркало паттерна analyze (parcels.py:1418-1438). coord_row = ( db.execute( text(""" SELECT ST_X(ST_Centroid(g.geom)) AS lon, ST_Y(ST_Centroid(g.geom)) AS lat FROM ( - SELECT geom FROM cad_quarters_geom WHERE cad_number = :c + SELECT geom FROM cad_quarters_geom + WHERE cad_number = :c AND geom IS NOT NULL UNION ALL - SELECT geom FROM cad_buildings WHERE cad_num = :c + SELECT geom FROM cad_buildings + WHERE cad_num = :c AND geom IS NOT NULL UNION ALL - SELECT geom FROM cad_parcels_geom WHERE cad_num = :c + SELECT geom FROM cad_parcels_geom + WHERE cad_num = :c AND geom IS NOT NULL ) g LIMIT 1 """), @@ -2954,7 +2961,11 @@ def get_isochrones( .first() ) - if not coord_row: + # belt-and-suspenders: row может вернуться с NULL lat/lon только если в БД + # есть строка с НЕ-NULL geom но битый ST_Centroid (топологически невалидный + # polygon). Граница `not coord_row` truthy-проверка на RowMapping её бы НЕ + # поймала (RowMapping с None-полями всё ещё truthy) → float(None) → 500. + if not coord_row or coord_row.get("lat") is None or coord_row.get("lon") is None: raise HTTPException( status_code=404, detail=f"Геометрия для {cad_num} не найдена.", @@ -3029,18 +3040,24 @@ async def get_poi_score( POI берутся из osm_poi_ekb в заданном радиусе (default 2000м). Отсортированы по weight DESC — наиболее значимые объекты первыми. """ - # Получить координаты центроида участка из геометрических таблиц + # Получить координаты центроида участка из геометрических таблиц. + # См. парный комментарий в get_isochrones — `geom IS NOT NULL` во ВСЕХ ветках + # UNION (#1201): NULL-строка ранней ветки скрывает валидную из поздней + + # ST_Centroid(NULL) → NULL → float(None) → 500. Зеркало analyze (parcels.py:1418-1438). coord_row = ( db.execute( text(""" SELECT ST_X(ST_Centroid(g.geom)) AS lon, ST_Y(ST_Centroid(g.geom)) AS lat FROM ( - SELECT geom FROM cad_quarters_geom WHERE cad_number = :c + SELECT geom FROM cad_quarters_geom + WHERE cad_number = :c AND geom IS NOT NULL UNION ALL - SELECT geom FROM cad_buildings WHERE cad_num = :c + SELECT geom FROM cad_buildings + WHERE cad_num = :c AND geom IS NOT NULL UNION ALL - SELECT geom FROM cad_parcels_geom WHERE cad_num = :c + SELECT geom FROM cad_parcels_geom + WHERE cad_num = :c AND geom IS NOT NULL ) g LIMIT 1 """), @@ -3050,7 +3067,8 @@ async def get_poi_score( .first() ) - if not coord_row: + # belt-and-suspenders: см. get_isochrones — RowMapping с None-полями truthy. + if not coord_row or coord_row.get("lat") is None or coord_row.get("lon") is None: raise HTTPException( status_code=404, detail=f"Геометрия для {cad_num} не найдена.",