Прямоугольники 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 вызовов, точки по всем трём
регионам вперемешку) — в пределах бюджета.