feat(tradein/geo): реестр регионов — единственный источник гео-границ (#3051, шард 1a) #3116

Merged
bot-backend merged 2 commits from feat/3051-region-registry into main 2026-08-26 15:24:00 +00:00
Collaborator

Шард 1a части 1/3 «Москвы» (#3051, эпик #2989): реестр + вывод литералов из трёх файлов + покрытие точки Москвы.

Что

Новый app/services/regions.py (лист дерева импортов): per-регион bbox_tight/wide/region/product_core, города, city_token, набор доступных тиров обогащения, region_for_point / region_by_city / unsupported_tier_reason.

Потребители переведены на реестр, прежние имена остались алиасами на ОБЪЕКТЫ реестра (identity закреплена тестом — копии, разъезжающиеся при следующей правке, невозможны по построению):

файл что было что стало
location_index.py _EKB_BBOX_* литералы + _in_ekb_bbox _covered_region() по реестру; SQL-медианы считаются в ядре региона ТОЧКИ
geocoder.py EKB_BBOX_TIGHT/WIDE, OBLAST66_BBOX, SVERDLOVSK_OBLAST_CITIES литералами те же имена = поля REGIONS[66]
matching/normalize.py EKB_CITY_TOKEN = "екатеринбург" REGIONS[66].city_token; _CITY_TOKENS НЕ тронут намеренно — это зеркало sweep-раскатки (CITY_ANCHORS пайплайна), другой концерн

Гарантии

  • 66 байт-в-байт: значения региона 66 равны прежним литералам и закреплены тестом — PR переносит границы, не меняет их. Точка южнее продукт-ядра (56.68) по-прежнему out_of_coverage в обеих эрах.
  • Приёмка #3051: 55.75/37.62 больше не out_of_coverage — регион 77 узнан, счёт идёт в его ядре; при сегодняшнем нуле московских листингов итог — честный insufficient_data (это правильнее буквального «ok» из приёмки: ok без данных был бы враньём).
  • Явная деградация: у 77 enrichment_tiers = frozenset() — не заглушка, а факт с готовой формулировкой unsupported_tier_reason для потребителей (вайринг в estimator — следующим шардом, вместе с п.5).
  • Гейт ЕКБ-тиров геокодера (#2582) для Москвы уже корректен по построению: fail-closed открывает ЕКБ-локальные тиры только при подтверждённом ЕКБ.

Проверки

  • test_3051_region_registry.py (9): вложенность bbox'ов, непересечение регионов, байт-в-байт 66, identity алиасов, Москва-точка (красный на main по значению: out_of_coverageinsufficient_data), инварианты ЕКБ/Перми, ё-фолд городов, громкая деградация тиров
  • окрестности: 319 гео-тестов (geocoder_bbox/city_hint/substitution/cadastral/nominatim/location_index/reverse) + 115 matching — все зелёные

Не здесь (следующие шарды #3051)

п.2 city_fias_id сквозняком · п.3 doc_type в deals · п.4 region_code NOT NULL у houses · п.5 депромоут не-ЕКБ описаний + вайринг деградации в estimator · п.6 параметры загрузчиков. Kit-копия scraper_kit/geo.py не тронута: скраперы собирают только 66, Москва поедет FDW-путём (#3053).

🤖 Generated with Claude Code

Шард 1a части 1/3 «Москвы» (#3051, эпик #2989): реестр + вывод литералов из трёх файлов + покрытие точки Москвы. ## Что Новый `app/services/regions.py` (лист дерева импортов): per-регион `bbox_tight/wide/region/product_core`, города, `city_token`, набор доступных тиров обогащения, `region_for_point` / `region_by_city` / `unsupported_tier_reason`. Потребители переведены на реестр, прежние имена остались алиасами на ОБЪЕКТЫ реестра (identity закреплена тестом — копии, разъезжающиеся при следующей правке, невозможны по построению): | файл | что было | что стало | |---|---|---| | `location_index.py` | `_EKB_BBOX_*` литералы + `_in_ekb_bbox` | `_covered_region()` по реестру; SQL-медианы считаются в ядре региона ТОЧКИ | | `geocoder.py` | `EKB_BBOX_TIGHT/WIDE`, `OBLAST66_BBOX`, `SVERDLOVSK_OBLAST_CITIES` литералами | те же имена = поля `REGIONS[66]` | | `matching/normalize.py` | `EKB_CITY_TOKEN = "екатеринбург"` | `REGIONS[66].city_token`; `_CITY_TOKENS` НЕ тронут намеренно — это зеркало sweep-раскатки (CITY_ANCHORS пайплайна), другой концерн | ## Гарантии - **66 байт-в-байт**: значения региона 66 равны прежним литералам и закреплены тестом — PR переносит границы, не меняет их. Точка южнее продукт-ядра (56.68) по-прежнему `out_of_coverage` в обеих эрах. - **Приёмка #3051**: `55.75/37.62` больше не `out_of_coverage` — регион 77 узнан, счёт идёт в его ядре; при сегодняшнем нуле московских листингов итог — честный `insufficient_data` (это правильнее буквального «ok» из приёмки: ok без данных был бы враньём). - **Явная деградация**: у 77 `enrichment_tiers = frozenset()` — не заглушка, а факт с готовой формулировкой `unsupported_tier_reason` для потребителей (вайринг в estimator — следующим шардом, вместе с п.5). - Гейт ЕКБ-тиров геокодера (#2582) для Москвы уже корректен по построению: fail-closed открывает ЕКБ-локальные тиры только при подтверждённом ЕКБ. ## Проверки - `test_3051_region_registry.py` (9): вложенность bbox'ов, непересечение регионов, байт-в-байт 66, identity алиасов, Москва-точка (**красный на main по значению**: `out_of_coverage` ≠ `insufficient_data`), инварианты ЕКБ/Перми, ё-фолд городов, громкая деградация тиров - окрестности: 319 гео-тестов (geocoder_bbox/city_hint/substitution/cadastral/nominatim/location_index/reverse) + 115 matching — все зелёные ## Не здесь (следующие шарды #3051) п.2 `city_fias_id` сквозняком · п.3 `doc_type` в deals · п.4 `region_code NOT NULL` у houses · п.5 депромоут не-ЕКБ описаний + вайринг деградации в estimator · п.6 параметры загрузчиков. Kit-копия `scraper_kit/geo.py` не тронута: скраперы собирают только 66, Москва поедет FDW-путём (#3053). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-08-26 13:40:04 +00:00
feat(tradein/geo): реестр регионов — единственный источник гео-границ (#3051, шард 1a)
Some checks failed
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m39s
0529bb249c
Границы покрытия лежали литералами в трёх файлах (location_index / geocoder /
matching.normalize), и каждая молча отвергла бы Москву. Новый модуль
app.services.regions — лист дерева импортов — держит per-регион bbox'ы
(tight/wide/region/product_core), города, city_token и набор доступных тиров
обогащения; потребители держат прежние имена как алиасы на объекты реестра
(identity закреплена тестом — копии, разъезжающиеся при правке, невозможны).

Регион 66 — байт-в-байт прежние литералы (закреплено тестом: этот PR только
переносит границы, менять их = отдельное решение). Регион 77 (Москва): МКАД-
ядро + генеральный bbox с Новой Москвой и Зеленоградом; тиров обогащения НЕТ
ни одного — и это явный факт реестра с готовой формулировкой
(unsupported_tier_reason), а не молчаливое «посчитаем без источника».

Приёмка #3051: точка 55.75/37.62 больше не out_of_coverage — location_index
узнаёт регион 77 и считает в его ядре (сегодня листингов Москвы нет → честный
insufficient_data). Область 50 отложена по решению в #2996.

Не здесь (следующие шарды): city_fias_id сквозняком (п.2), doc_type в deals
(п.3), region_code у houses (п.4), депромоут описаний (п.5), параметры
загрузчиков (п.6). Гейт ЕКБ-тиров геокодера (#2582) уже деградирует правильно
для Москвы — fail-closed открывает их только при подтверждённом ЕКБ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner

CI Trade-In / backend-tests красный — причина найдена

Упал существующий тест, а не новый. tests/services/test_location_index.py зовёт функцию, которую этот PR переименовал:

>       assert lc._in_ekb_bbox(_LAT_IN_EKB, _LON_IN_EKB) is True
E       AttributeError: module 'app.services.location_index' has no attribute '_in_ekb_bbox'
tests/services/test_location_index.py:51

>       assert lc._in_ekb_bbox(56.70, 60.50) is True     # :55
>       assert lc._in_ekb_bbox(57.910, 59.970) is False  # :61

Остальные семь проверок зелёные — падает только этот джоб.

Просьба при починке: не удалять покрытие

Быстрый путь — снести устаревшие строки, но они проверяют вещи, которые стоит сохранить и на новом API:

  • (56.70, 60.50) — угловая точка bbox. Проверяет, что границы включающие. При переходе на is_within_bbox это ровно то место, где легко получить < вместо <= и молча потерять полосу по краю охвата.
  • (57.910, 59.970) — точка вне охвата. Проверяет, что за пределами ядра возвращается «не покрыто». Сейчас это тем важнее: по описанию PR точка Москвы теперь попадает в покрытие региона 77, и граница между «не покрыто» и «покрыто, но данных нет» стала содержательной.

Логично переписать их на _covered_region(...) is None / is not None — тогда проверяется тот же инвариант, но уже через новый вход.

Что проверил со своей стороны

Главное заявление PR подтверждается: bbox_product_core=(56.70, 56.95, 60.50, 60.75) у региона 66 байт-в-байт равен прежним литералам location_index (SOUTH=56.70, NORTH=56.95, WEST=60.50, EAST=60.75). Для Екатеринбурга поведение не меняется — а это и был главный риск такого рефакторинга.

Смена поведения для Москвы (insufficient_data вместо out_of_coverage) в описании заявлена явно, так что она читается как решение, а не как побочный эффект.

В саму ветку не лезу, чтобы не столкнуться с твоей сессией — правка на минуту.

## CI Trade-In / backend-tests красный — причина найдена Упал существующий тест, а не новый. `tests/services/test_location_index.py` зовёт функцию, которую этот PR переименовал: ``` > assert lc._in_ekb_bbox(_LAT_IN_EKB, _LON_IN_EKB) is True E AttributeError: module 'app.services.location_index' has no attribute '_in_ekb_bbox' tests/services/test_location_index.py:51 > assert lc._in_ekb_bbox(56.70, 60.50) is True # :55 > assert lc._in_ekb_bbox(57.910, 59.970) is False # :61 ``` Остальные семь проверок зелёные — падает только этот джоб. ## Просьба при починке: не удалять покрытие Быстрый путь — снести устаревшие строки, но они проверяют вещи, которые стоит сохранить и на новом API: - **`(56.70, 60.50)` — угловая точка bbox.** Проверяет, что границы **включающие**. При переходе на `is_within_bbox` это ровно то место, где легко получить `<` вместо `<=` и молча потерять полосу по краю охвата. - **`(57.910, 59.970)` — точка вне охвата.** Проверяет, что за пределами ядра возвращается «не покрыто». Сейчас это тем важнее: по описанию PR точка Москвы теперь попадает в покрытие региона 77, и граница между «не покрыто» и «покрыто, но данных нет» стала содержательной. Логично переписать их на `_covered_region(...) is None` / `is not None` — тогда проверяется тот же инвариант, но уже через новый вход. ## Что проверил со своей стороны Главное заявление PR подтверждается: `bbox_product_core=(56.70, 56.95, 60.50, 60.75)` у региона 66 **байт-в-байт** равен прежним литералам `location_index` (`SOUTH=56.70, NORTH=56.95, WEST=60.50, EAST=60.75`). Для Екатеринбурга поведение не меняется — а это и был главный риск такого рефакторинга. Смена поведения для Москвы (`insufficient_data` вместо `out_of_coverage`) в описании заявлена явно, так что она читается как решение, а не как побочный эффект. В саму ветку не лезу, чтобы не столкнуться с твоей сессией — правка на минуту.
Light1YT added 1 commit 2026-08-26 13:47:35 +00:00
test(3051): tests/services/test_location_index.py переведён на _covered_region
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-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 / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m38s
24369a6bf2
CI поймал то, что мой локальный прогон пропустил: сьют в ПОДДИРЕКТОРИИ
tests/services/ звал удалённый _in_ekb_bbox. Граничные точки сохранены те же
(продукт-ядро 66 байт-в-байт), проверка дополнена кодом региона.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 8a4215fe20 into main 2026-08-26 15:24:00 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 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#3116
No description provided.