fix(tradein/geocoder): инвариант «чужой топоним + результат внутри ЕКБ = недостоверно» (#2590) #2837

Merged
bot-backend merged 1 commit from fix/2590-geocoder-city-invariant into main 2026-08-12 14:26:58 +00:00

1 commit

Author SHA1 Message Date
48db4991fc fix(tradein/geocoder): инвариант «чужой топоним + результат внутри ЕКБ = недостоверно»
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 / 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 3m56s
Гейты #2582/#2589 стоят на входе и решают, пускать ли ЕКБ-only реестры.
Внешние провайдеры ими не покрыты: «реж, ленина» уходит в Nominatim/Yandex,
тот не находит Режа и отдаёт улицу Ленина в Екатеринбурге — с координатами
ЕКБ, пометкой «точно» и записью в кэш на 90 дней.

Отличить на входе «Реж» (город) от «Малышева» (улица) без списка городов
нельзя — оба «слово до запятой». ПОСЛЕ ответа можно: провайдер сам пишет,
какой НП он использовал. Отсюда проверка постфактум: назван топоним ≠
Екатеринбург, его имя не дожило до ответа провайдера, результат лежит внутри
ЕКБ (по координатам И по собственному ответу провайдера) ⇒ подмена.

Ни одного имени города в коде — только уровни РФ-адреса (страна → регион →
район → НП → улица → дом) и сам целевой город. Посёлки в городской черте
разделяются не географией, а сохранностью топонима: «Кедровка, Екатеринбург,
Советская» имя сохраняет и проходит, «Екатеринбург, Советская улица» на тот
же запрос — нет (настоящая Кедровка в 20 км от той улицы).

Проверка стоит одной точкой на выходе geocode() — покрывает все тиры разом,
включая попадание в кэш: 1251 отравленная запись перестаёт отдаваться как
точная без единого DELETE (обратимо откатом кода).

Последствие сработки — confidence="locality" + city_ambiguous=True. locality
не косметика: estimator._geocode_is_coarse включает #693 coarse-downgrade, а
tasks.geocode_missing ставит листингу geo_precision='city', что исключает его
из пула аналогов (geo_precision IS DISTINCT FROM 'city'). Координаты не
выбрасываются — деградация честная и видимая.

Замер на проде (geocode_cache, 9514 строк, 2026-08-12): 1251 запись подпадает
под инвариант — 1245 provider=yandex, 6 nominatim, 0 у локальных тиров;
365 разных населённых пунктов.

Refs #2590
2026-08-12 19:20:58 +05:00