(critical) SRID: геометрия кварталов и зданий из rosreestr2coord (WGS84) ошибочно ST_Transform'ится из 3857 → коллапс к Null Island #1336

Closed
opened 2026-06-14 18:08:36 +00:00 by bot-backend · 1 comment
Collaborator

Описание

Главный цикл nspd_geo.py тянет ВСЕ цели (parcel/quarter/building, все thematic_id) только через fetch_via_rosreestr2coord (строка 427-431), который возвращает GeoJSON в WGS84 (EPSG:4326) для всех area_type. _persist_target роутит по thematic_id: _save_parcel (tid=1) корректно делает ST_SetSRID(...,4326) БЕЗ transform, а _save_quarter (tid=2) и _save_building (tid=5) применяют ST_Transform(ST_SetSRID(geom,3857),4326) — трактуя WGS84-градусы как метры Web-Mercator. Для ЕКБ (lon≈60.6, lat≈56.8) transform схлопывает полигон до ~(0.0005°,0.0005°) — точка возле Null Island у берегов Африки. Каждый quarter-полигон (cad_quarters_geom) и building-полигон (cad_buildings.geom), записанный этим путём, испорчен, перетирая корректные данные через ON CONFLICT DO UPDATE.

Корень

Библиотека rosreestr2coord (Area(...)) использует дефолт coord_out='EPSG:4326' и в fetch_via_rosreestr2coord он НЕ переопределяется → всегда WGS84 для всех area_type. Комментарий-докстринг _save_parcel («Quarters/buildings — другой path, у них Mercator (3857)») — устаревший: относится к старому NSPD bulk API (удалён, см. комментарий nspd_geo.py:424-426), но ST_Transform(...,3857,...) в quarter/building-saver остался.

Evidence

nspd_geo.py:155 (_save_quarter):

ST_Multi(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:g), 3857), 4326))

nspd_geo.py:203 (_save_building):

ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:g), 3857), 4326)

nspd_geo.py:261 (_save_parcel — корректно, без transform):

ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326))

nspd_geo.py:427 — единый источник для всех tid: fetch_via_rosreestr2coord(cad, area_type=tid, timeout=15).
Докстринг _save_parcel сам описывает этот режим отказа: «иначе геометрия сжимается до точки в районе (0,0) на острове Null».

Suggested fix

Убрать ST_Transform(...,3857,...) в _save_quarter и _save_building, использовать ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326)) как в _save_parcel (ST_Multi там, где целевая колонка MultiPolygon). Альтернатива — явно зафиксировать coord_out и держать единую SRID-конвенцию во всех трёх saver'ах. Удалить устаревший комментарий в докстринге _save_parcel.

Обоснование severity

critical: путь полностью достижим в проде (нет feature-flag/guard), молча портит ВСЕ quarter/building-полигоны, перетирая корректные данные через UPSERT. confidence 0.97.


week ревью 1 · multi-agent аудит 2026-06-14 · severity=critical · category=data-integrity

Затронутые файлы:

  • /Users/anton/Птица/gendesign/backend/app/workers/tasks/nspd_geo.py
## Описание Главный цикл `nspd_geo.py` тянет ВСЕ цели (parcel/quarter/building, все thematic_id) только через `fetch_via_rosreestr2coord` (строка 427-431), который возвращает GeoJSON в WGS84 (EPSG:4326) для всех area_type. `_persist_target` роутит по thematic_id: `_save_parcel` (tid=1) корректно делает `ST_SetSRID(...,4326)` БЕЗ transform, а `_save_quarter` (tid=2) и `_save_building` (tid=5) применяют `ST_Transform(ST_SetSRID(geom,3857),4326)` — трактуя WGS84-градусы как метры Web-Mercator. Для ЕКБ (lon≈60.6, lat≈56.8) transform схлопывает полигон до ~(0.0005°,0.0005°) — точка возле Null Island у берегов Африки. Каждый quarter-полигон (`cad_quarters_geom`) и building-полигон (`cad_buildings.geom`), записанный этим путём, испорчен, перетирая корректные данные через ON CONFLICT DO UPDATE. ## Корень Библиотека `rosreestr2coord` (`Area(...)`) использует дефолт `coord_out='EPSG:4326'` и в `fetch_via_rosreestr2coord` он НЕ переопределяется → всегда WGS84 для всех area_type. Комментарий-докстринг `_save_parcel` («Quarters/buildings — другой path, у них Mercator (3857)») — устаревший: относится к старому NSPD bulk API (удалён, см. комментарий nspd_geo.py:424-426), но `ST_Transform(...,3857,...)` в quarter/building-saver остался. ## Evidence `nspd_geo.py:155` (_save_quarter): ```sql ST_Multi(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:g), 3857), 4326)) ``` `nspd_geo.py:203` (_save_building): ```sql ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:g), 3857), 4326) ``` `nspd_geo.py:261` (_save_parcel — корректно, без transform): ```sql ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326)) ``` `nspd_geo.py:427` — единый источник для всех tid: `fetch_via_rosreestr2coord(cad, area_type=tid, timeout=15)`. Докстринг `_save_parcel` сам описывает этот режим отказа: «иначе геометрия сжимается до точки в районе (0,0) на острове Null». ## Suggested fix Убрать `ST_Transform(...,3857,...)` в `_save_quarter` и `_save_building`, использовать `ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326))` как в `_save_parcel` (ST_Multi там, где целевая колонка MultiPolygon). Альтернатива — явно зафиксировать `coord_out` и держать единую SRID-конвенцию во всех трёх saver'ах. Удалить устаревший комментарий в докстринге `_save_parcel`. ## Обоснование severity critical: путь полностью достижим в проде (нет feature-flag/guard), молча портит ВСЕ quarter/building-полигоны, перетирая корректные данные через UPSERT. confidence 0.97. --- _week ревью 1 · multi-agent аудит 2026-06-14 · severity=**critical** · category=`data-integrity`_ Затронутые файлы: - `/Users/anton/Птица/gendesign/backend/app/workers/tasks/nspd_geo.py`
bot-backend added the
week ревью 1
label 2026-06-14 18:08:36 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:01:48 +00:00
Owner

🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)

Текущее состояние кода: backend/app/workers/tasks/nspd_geo.py:154 (_save_quarter) и :202 (_save_building) — оба используют ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:g), 3857), 4326). _save_parcel:260 корректно ST_Multi(ST_SetSRID(..., 4326)) без transform. Подтверждено и на git show forgejo/main:backend/app/workers/tasks/nspd_geo.py (идентично — баг в проде).

Корень: rosreestr2coord (Area(...)) отдаёт WGS84 (EPSG:4326) для ВСЕХ area_type; единый источник fetch_via_rosreestr2coord не переопределяет coord_out. Quarter/building-saver трактуют градусы как метры Mercator → ST_Transform коллапсирует полигон к Null Island. Комментарии в коде (:153 «3857→4326 как Mercator», docstring _save_parcel:239-240) — устаревшие, относятся к удалённому NSPD bulk API.

Что править:

  • В _save_quarter (:154) заменить на ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326)) (целевая колонка cad_quarters_geom.geom = MultiPolygon, ST_Multi нужен).
  • В _save_building (:202) заменить на ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326) (колонка cad_buildings.geom = GEOMETRY(Geometry,4326) полиморфная — ST_Multi не обязателен, mirror _save_parcel).
  • Обновить устаревшие комментарии :150-153 и docstring _save_parcel:237-240 (убрать упоминание Mercator path).
  • ON CONFLICT DO UPDATE перетёр корректные данные → после фикса нужен повторный sweep quarter/building (полный re-fetch перезапишет испорченные геометрии).

Что посмотреть:

  • Код: nspd_geo.py функции _save_quarter / _save_building / _save_parcel; добавить тест на SRID/bbox результата (полигон ЕКБ lon≈60.6 lat≈56.8 не должен коллапсировать к ~0,0). Проверить data-recovery: SELECT count(*) FROM cad_quarters_geom WHERE ST_X(ST_Centroid(geom)) < 1 (испорченные строки у Null Island).
  • Vault: релевантных заметок не найдено.
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** `backend/app/workers/tasks/nspd_geo.py:154` (`_save_quarter`) и `:202` (`_save_building`) — оба используют `ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:g), 3857), 4326)`. `_save_parcel:260` корректно `ST_Multi(ST_SetSRID(..., 4326))` без transform. Подтверждено и на `git show forgejo/main:backend/app/workers/tasks/nspd_geo.py` (идентично — баг в проде). **Корень:** `rosreestr2coord` (`Area(...)`) отдаёт WGS84 (EPSG:4326) для ВСЕХ area_type; единый источник `fetch_via_rosreestr2coord` не переопределяет `coord_out`. Quarter/building-saver трактуют градусы как метры Mercator → ST_Transform коллапсирует полигон к Null Island. Комментарии в коде (`:153` «3857→4326 как Mercator», docstring `_save_parcel:239-240`) — устаревшие, относятся к удалённому NSPD bulk API. **Что править:** - В `_save_quarter` (`:154`) заменить на `ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326))` (целевая колонка `cad_quarters_geom.geom` = MultiPolygon, ST_Multi нужен). - В `_save_building` (`:202`) заменить на `ST_SetSRID(ST_GeomFromGeoJSON(:g), 4326)` (колонка `cad_buildings.geom` = `GEOMETRY(Geometry,4326)` полиморфная — ST_Multi не обязателен, mirror `_save_parcel`). - Обновить устаревшие комментарии `:150-153` и docstring `_save_parcel:237-240` (убрать упоминание Mercator path). - ON CONFLICT DO UPDATE перетёр корректные данные → после фикса нужен повторный sweep quarter/building (полный re-fetch перезапишет испорченные геометрии). **Что посмотреть:** - Код: `nspd_geo.py` функции `_save_quarter` / `_save_building` / `_save_parcel`; добавить тест на SRID/bbox результата (полигон ЕКБ lon≈60.6 lat≈56.8 не должен коллапсировать к ~0,0). Проверить data-recovery: `SELECT count(*) FROM cad_quarters_geom WHERE ST_X(ST_Centroid(geom)) < 1` (испорченные строки у Null Island). - Vault: релевантных заметок не найдено.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1336
No description provided.