Регион точки резолвится по настоящей границе, а не по прямоугольнику (#3052) #3522

Merged
lekss361 merged 1 commit from feat/3052-region-polygons into main 2026-09-15 06:44:32 +00:00
Owner

Closes #3052.

Проблема

region_for_point относил точку к региону по bbox_region — прямоугольнику. Прямоугольники Москвы и области пересекаются, поэтому Химки, Реутов, Котельники и Люберцы получали код 77: московский коэффициент перехода к цене сделки (0.7596 вместо 0.8973 области) и индекс локации, посчитанный по московской медиане, то есть −20% ни за что.

Замер на проде: 8 647 московских лотов лежат вне продуктового ядра, 11 172 сделки области попадают внутрь московского ядра.

Решение

Граница региона теперь настоящая — полигоны OSM/Nominatim лежат рядом с модулем в region_boundaries/boundaries.geojson.json (ODbL, поля _source/_license сохранены внутри файла). Резолв идёт в два уровня: дешёвый bbox-предфильтр, затем ray casting по всем кольцам геометрии сразу, even-odd. Плоский список колец одинаково закрывает и дыры полигона области (Москва вырезана одной из девяти), и несвязные части мультиполигона Москвы.

Новых зависимостей нет, shapely в tradein-mvp отсутствует и не добавлен. Геометрия парсится один раз на импорте модуля. bbox_region остался как предфильтр, geocoder.py и msk_raw_import.py не тронуты.

Проверено

Девять контрольных точек резолвятся ровно в один регион каждая: Химки, Балашиха и Серпухов в 50, центр Москвы, Ломоносовский проспект, Зеленоград и Троицк в 77, Екатеринбург в 66, Пермь в None.

Скорость 25 мкс на вызов на 240 тысячах точек, в четыре раза дешевле принятого потолка.

pytest tests/ — 6107 passed, 43 skipped. ruff check и format чисты.

Про исключение (50, 77) в тесте непересечения

Оно осталось и теперь объяснено в коде. Тот тест проверяет сырые прямоугольники, а bbox_region Москвы остаётся подмножеством прямоугольника области независимо от полигонов — полигон работает после предфильтра, а не вместо него. Честная непересекаемость по итоговому резолву проверена отдельным тестом test_3052_region_polygon_boundary.py. Тест, который раньше фиксировал Химки как known limitation, теперь фиксирует исправленное поведение.

Closes #3052. ## Проблема `region_for_point` относил точку к региону по `bbox_region` — прямоугольнику. Прямоугольники Москвы и области пересекаются, поэтому Химки, Реутов, Котельники и Люберцы получали код 77: московский коэффициент перехода к цене сделки (0.7596 вместо 0.8973 области) и индекс локации, посчитанный по московской медиане, то есть −20% ни за что. Замер на проде: 8 647 московских лотов лежат вне продуктового ядра, 11 172 сделки области попадают внутрь московского ядра. ## Решение Граница региона теперь настоящая — полигоны OSM/Nominatim лежат рядом с модулем в `region_boundaries/boundaries.geojson.json` (ODbL, поля `_source`/`_license` сохранены внутри файла). Резолв идёт в два уровня: дешёвый bbox-предфильтр, затем ray casting по всем кольцам геометрии сразу, even-odd. Плоский список колец одинаково закрывает и дыры полигона области (Москва вырезана одной из девяти), и несвязные части мультиполигона Москвы. Новых зависимостей нет, shapely в tradein-mvp отсутствует и не добавлен. Геометрия парсится один раз на импорте модуля. `bbox_region` остался как предфильтр, `geocoder.py` и `msk_raw_import.py` не тронуты. ## Проверено Девять контрольных точек резолвятся ровно в один регион каждая: Химки, Балашиха и Серпухов в 50, центр Москвы, Ломоносовский проспект, Зеленоград и Троицк в 77, Екатеринбург в 66, Пермь в None. Скорость 25 мкс на вызов на 240 тысячах точек, в четыре раза дешевле принятого потолка. `pytest tests/` — 6107 passed, 43 skipped. ruff check и format чисты. ## Про исключение (50, 77) в тесте непересечения Оно осталось и теперь объяснено в коде. Тот тест проверяет сырые прямоугольники, а `bbox_region` Москвы остаётся подмножеством прямоугольника области независимо от полигонов — полигон работает после предфильтра, а не вместо него. Честная непересекаемость по итоговому резолву проверена отдельным тестом `test_3052_region_polygon_boundary.py`. Тест, который раньше фиксировал Химки как known limitation, теперь фиксирует исправленное поведение.
lekss361 added 1 commit 2026-09-15 06:36:07 +00:00
fix(tradein): region_for_point резолвит по настоящему полигону, не по bbox (#3052)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 6m3s
30832c27ed
Прямоугольники bbox_region Москвы (77) и области (50) пересекались:
bbox_region(50) геометрически содержит bbox_region(77) как прямоугольник, и
region_for_point отдавал точку первому по площади bbox совпадению без учёта
того, что реальные админ-границы не пересекаются. Из-за этого Химки, Реутов,
Котельники, Люберцы резолвились в Москву — 8 647 московских лотов уходили в
отказ по покрытию, а 11 172 областные сделки ложно попадали в московское
ядро, хотя коридор ДКП фильтрует строго по коду региона.

Границы — полигоны OSM/Nominatim (region_boundaries/boundaries.geojson.json,
ODbL, _source/_license сохранены в файле по требованию лицензии), парсятся
один раз на импорте модуля. Point-in-polygon — ray casting (PNPOLY) с even-odd
правилом по плоскому списку колец: один и тот же код корректно обрабатывает
и дыру (Москва вырезана из полигона области) и мультиполигон (10 несвязных
частей Москвы — Зеленоград, анклавы). Без shapely — его нет в зависимостях
tradein backend, тянуть ради одной функции незачем.

bbox_region остаётся дешёвым предфильтром перед полигоном (его читают
geocoder.py и msk_raw_import.py), просто больше не финальный ответ: порядок
_POINT_LOOKUP_ORDER (компактный bbox раньше объёмного) теперь экономит
polygon-проверки на частом случае Москва/область, а не определяет корректность.

Тест на Химки/Балашиху (test_3051_region_registry_moscow_oblast.py) раньше
фиксировал это как "известное ограничение" bbox-резолва — обновлён под
исправленное поведение (code 50, не 77). Новый test_3052_region_polygon_
boundary.py — приёмка по девяти контрольным точкам issue #3052 + инвариант
"полигон вложен в свой bbox_region" (bbox не должен молча отсекать то, что
полигон бы принял).

Исключение (50, 77) в test_regions_do_not_overlap НЕ снято: тест проверяет
raw-прямоугольники (is_within_bbox/bbox_region), а не итоговый резолв точки —
bbox_region(77) геометрически остаётся подмножеством bbox_region(50) как
прямоугольник независимо от полигонов, это не тот инвариант, который фикс
меняет. Честная непересекаемость теперь проверена на уровне результата
region_for_point в test_3052_region_polygon_boundary.py.

Замер: ~25 мкс на вызов region_for_point (240k вызовов, точки по всем трём
регионам вперемешку) — в пределах бюджета.
lekss361 merged commit 3d114672fe into main 2026-09-15 06:44:32 +00:00
lekss361 deleted branch feat/3052-region-polygons 2026-09-15 06:44:32 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#3522
No description provided.