fix(ptica): гео-проход не тратит геокод на занятых кандидатов и не «подтверждает» их (#2464) #2929

Merged
bot-backend merged 1 commit from fix/2464-geo-pass-taken-candidates into main 2026-08-19 11:52:07 +00:00
Collaborator

Что не так

Разбирая неоднозначные ядра, find_geo_matches геокодил всех objective-кандидатов ядра — включая тех, чьё имя уже занято в objective_complex_mapping.

Записать занятое имя нельзя в принципе: apply_geo_matches вставляет с ON CONFLICT (objective_complex_name, objective_group) DO NOTHING, а _TAKEN_NAMES_SQL выбирает ровно по этому ключу. То есть отсев занятых не может отменить ни одной записи, которая иначе бы произошла — он убирает только то, что запись и так отбрасывала.

Три следствия на боевом пути:

ситуация было стало
занятый в радиусе, свободный далеко занятый уходил в confirmed → прогон рапортует подтверждение, запись молча ничего не делает честный reject too_far
занятый рядом со свободным «двое в радиусе» → ambiguous_multi, хотя выбор единственный резолв на свободного
заняты все кандидаты геокод обоих, отчёт ambiguous_multi reject all_candidates_taken, ноль вызовов DaData

Замер на проде (19.08)

несопоставленных domrf ЕКБ-объектов      930
  из них неоднозначных                    14   (1.5%)
слотов кандидатов                         28
  из них занятых (геокод впустую)         14   (50%)
разных адресов к геокоду                   6   (лимит DaData: 200/прогон)
  из них принадлежат занятым               3
после отсева свободен ровно один          14   из 14

Честно про масштаб: 14 строк из 930, гео-проход запускается вручную и записал за всё время 2 строки (auto_core_geo_v6). Правка маленькая; ценность — не в объёме, а в том, что «подтверждено» перестаёт означать «ничего не записано».

Заодно: причина отказа перестаёт врать

ambiguous_multi ставилась и при пустом in_radius. После отсева занятых случай «остался один кандидат, и он далеко» становится частым, и старая метка читалась бы оператором как факт неоднозначности. Теперь too_far при нуле близких, ambiguous_multi — только при нескольких.

Проверка

Три новых теста, каждый доказан двусторонне против origin/main:

тест на origin/main с правкой
..._ignores_taken_candidate_and_resolves assert 0 == 1 — свободный не резолвится зелёный
..._taken_candidate_not_confirmed_when_only_one_near assert [GeoMatch(...)] == []занятый попал в confirmed зелёный
..._all_candidates_taken_rejects_without_geocode AssertionError: DaData не должна вызываться зелёный

Контроли: остальные 30 тестов файла зелёные по обе стороны — при taken_names=[] поведение не меняется.

pytest tests/services3055 passed, 14 skipped, rc=0 (код возврата снят без конвейера; сторож пропусков молчит).

Имена в тестах взяты в боевой форме: ядро схлопывается только при кавычках — Бутик-квартал "Меридиан"меридиан, а Бутик-квартал Меридиан без кавычек даёт другое ядро. Первая версия тестов на этом и упала.

Refs #2464

## Что не так Разбирая неоднозначные ядра, `find_geo_matches` геокодил **всех** objective-кандидатов ядра — включая тех, чьё имя уже занято в `objective_complex_mapping`. Записать занятое имя нельзя в принципе: `apply_geo_matches` вставляет с `ON CONFLICT (objective_complex_name, objective_group) DO NOTHING`, а `_TAKEN_NAMES_SQL` выбирает ровно по этому ключу. То есть отсев занятых **не может** отменить ни одной записи, которая иначе бы произошла — он убирает только то, что запись и так отбрасывала. Три следствия на боевом пути: | ситуация | было | стало | |---|---|---| | занятый в радиусе, свободный далеко | занятый уходил в `confirmed` → прогон рапортует подтверждение, запись молча ничего не делает | честный reject `too_far` | | занятый рядом со свободным | «двое в радиусе» → `ambiguous_multi`, хотя выбор единственный | резолв на свободного | | заняты все кандидаты | геокод обоих, отчёт `ambiguous_multi` | reject `all_candidates_taken`, ноль вызовов DaData | ## Замер на проде (19.08) ``` несопоставленных domrf ЕКБ-объектов 930 из них неоднозначных 14 (1.5%) слотов кандидатов 28 из них занятых (геокод впустую) 14 (50%) разных адресов к геокоду 6 (лимит DaData: 200/прогон) из них принадлежат занятым 3 после отсева свободен ровно один 14 из 14 ``` Честно про масштаб: 14 строк из 930, гео-проход запускается вручную и записал за всё время 2 строки (`auto_core_geo_v6`). Правка маленькая; ценность — не в объёме, а в том, что «подтверждено» перестаёт означать «ничего не записано». ## Заодно: причина отказа перестаёт врать `ambiguous_multi` ставилась и при **пустом** `in_radius`. После отсева занятых случай «остался один кандидат, и он далеко» становится частым, и старая метка читалась бы оператором как факт неоднозначности. Теперь `too_far` при нуле близких, `ambiguous_multi` — только при нескольких. ## Проверка Три новых теста, каждый доказан двусторонне против `origin/main`: | тест | на `origin/main` | с правкой | |---|---|---| | `..._ignores_taken_candidate_and_resolves` | `assert 0 == 1` — свободный не резолвится | зелёный | | `..._taken_candidate_not_confirmed_when_only_one_near` | `assert [GeoMatch(...)] == []` — **занятый попал в confirmed** | зелёный | | `..._all_candidates_taken_rejects_without_geocode` | `AssertionError: DaData не должна вызываться` | зелёный | Контроли: остальные 30 тестов файла зелёные **по обе стороны** — при `taken_names=[]` поведение не меняется. `pytest tests/services` → **3055 passed, 14 skipped, rc=0** (код возврата снят без конвейера; сторож пропусков молчит). Имена в тестах взяты в боевой форме: ядро схлопывается только при кавычках — `Бутик-квартал "Меридиан"` → `меридиан`, а `Бутик-квартал Меридиан` без кавычек даёт другое ядро. Первая версия тестов на этом и упала. Refs #2464
bot-backend added 1 commit 2026-08-19 11:29:42 +00:00
fix(ptica): гео-проход не тратит геокод на занятых кандидатов и не «подтверждает» их (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 16m20s
015b310f62
Разбирая неоднозначные ядра, find_geo_matches геокодил ВСЕХ objective-кандидатов,
включая тех, чьё имя уже занято в objective_complex_mapping. Записать такое имя
нельзя в принципе: apply_geo_matches вставляет с
ON CONFLICT (objective_complex_name, objective_group) DO NOTHING, а _TAKEN_NAMES_SQL
выбирает ровно по этому ключу.

Последствия на боевом пути:
- занятый кандидат мог оказаться единственным в радиусе и уходил в confirmed —
  прогон рапортовал подтверждение, за которым запись молча не делала ничего;
- занятый рядом со свободным давал «двое в радиусе» → ambiguous_multi, хотя
  выбор был единственным;
- квота DaData (лимит 200 вызовов за прогон) тратилась на заведомо непишущихся.

Замер на проде 19.08: 930 несопоставленных domrf ЕКБ-объектов, из них 14
неоднозначных; 28 слотов кандидатов, 14 занятых; 3 из 6 разных адресов к геокоду
принадлежали занятым. После отсева у всех 14 остаётся ровно один кандидат.

Заодно причина отказа перестаёт врать: при пустом in_radius это 'too_far', а не
'ambiguous_multi'; когда заняты все кандидаты — отдельная 'all_candidates_taken'.
После отсева случай «остался один кандидат» становится частым, и старая метка
читалась бы оператором как факт неоднозначности.
Author
Collaborator

Почему починен только гео-проход, хотя находка #2464 говорит про оба

Находка формулирует дефект для core-pass и гео-прохода. Взял только гео — и это не экономия, а разный статус безопасности. Проверил данными, а не рассуждением.

Все 14 неоднозначных строк — пары «один занятый + один свободный». Смотрю, чем эти двое различаются:

свободный: Бутик-квартал "Меридиан"   пр-кт Ленина
занятый:   Меридиан                   ул. Архитектора Белянкина
           занятый уже привязан к domrf_obj_id=71375; спорный obj_id=10135

свободный: Жилой район Видный         ул. Михаила Сперанского, д. 43
занятый:   Видный                     ул. Викулова

свободный: Старт                      ул. Сергея Есенина, д. 22
занятый:   СТАРТ                      ул. Академика Вонсовского, д. 93

Застройщики различаются в 14 из 14 пар, адреса — во всех.

Главное здесь — третья пара. Старт и СТАРТ отличаются только регистром, и первым делом я заподозрил дубль одной записи: тогда моя правка связала бы domrf-объект с дублем, а не с настоящим ЖК. Адреса это опровергают — Есенина 22 и Вонсовского 93 находятся в разных концах города. Это разные ЖК с одинаковым именем.

Отсюда и граница:

  • Гео-проход различает кандидатов расстоянием, а адреса у них реально разные → отсев занятых даёт осмысленный выбор, а не подкидывание монеты. Чинится здесь.
  • core-pass располагает только именем и застройщиком. Для Старт vs СТАРТ имя не различает вовсе, а совпадение застройщика — сигнал слабый: он подтверждает «наш ли это девелопер», а не «который из двух его ЖК». Автоматический выбор «остался один свободный» там означал бы выбор вслепую с последующей записью в mapping. Не трогаю.

То есть половина находки закрыта, половина сознательно оставлена открытой. Отмечать пункт #2464 целиком закрытым по этому PR нельзя — в эпике напишу то же самое.

### Почему починен только гео-проход, хотя находка #2464 говорит про оба Находка формулирует дефект для core-pass **и** гео-прохода. Взял только гео — и это не экономия, а разный статус безопасности. Проверил данными, а не рассуждением. Все 14 неоднозначных строк — пары «один занятый + один свободный». Смотрю, чем эти двое различаются: ``` свободный: Бутик-квартал "Меридиан" пр-кт Ленина занятый: Меридиан ул. Архитектора Белянкина занятый уже привязан к domrf_obj_id=71375; спорный obj_id=10135 свободный: Жилой район Видный ул. Михаила Сперанского, д. 43 занятый: Видный ул. Викулова свободный: Старт ул. Сергея Есенина, д. 22 занятый: СТАРТ ул. Академика Вонсовского, д. 93 ``` Застройщики различаются в **14 из 14** пар, адреса — во всех. Главное здесь — третья пара. `Старт` и `СТАРТ` отличаются только регистром, и первым делом я заподозрил дубль одной записи: тогда моя правка связала бы domrf-объект с дублем, а не с настоящим ЖК. Адреса это опровергают — Есенина 22 и Вонсовского 93 находятся в разных концах города. Это **разные ЖК с одинаковым именем**. Отсюда и граница: - **Гео-проход** различает кандидатов расстоянием, а адреса у них реально разные → отсев занятых даёт осмысленный выбор, а не подкидывание монеты. Чинится здесь. - **core-pass** располагает только именем и застройщиком. Для `Старт` vs `СТАРТ` имя не различает вовсе, а совпадение застройщика — сигнал слабый: он подтверждает «наш ли это девелопер», а не «который из двух его ЖК». Автоматический выбор «остался один свободный» там означал бы выбор вслепую с последующей записью в mapping. Не трогаю. То есть половина находки закрыта, половина сознательно оставлена открытой. Отмечать пункт #2464 целиком закрытым по этому PR нельзя — в эпике напишу то же самое.
bot-backend merged commit bc489e1a6f into main 2026-08-19 11:52:07 +00:00
bot-backend deleted branch fix/2464-geo-pass-taken-candidates 2026-08-19 11:52:08 +00:00
Author
Collaborator

Прод-проверка после деплоя

Проверял не «код смержен», а что механизм отвечает по-другому на живых данных. Запуск find_core_matches в работающем контейнере (только SELECT, наружу не ходит):

counts: {'tier_a': 3, 'tier_b': 31, 'ambiguous': 14, 'skipped_taken': 507}
занятых имён в отчёте: 308        <- новое поле заполняется

неоднозначных: 14
  после отсева остался 1 кандидат: 14   -> гео-проход теперь может резолвить
  осталось несколько:               0
  не осталось никого:               0

До правки все 14 уходили в геокод парами, и половина вызовов DaData тратилась на кандидатов, которых запись отбрасывает по ON CONFLICT.

Чего проверка не доказывает: ни одна из 14 пока не резолвлена на самом деле — для этого нужен запуск гео-прохода, а он ручной и тратит квоту DaData. Критерий приёмки на будущее: после следующего run_geo_pass строк с match_method='auto_core_geo_v6' должно стать больше 2, и в логе должны появиться reject'ы с причиной too_far вместо ambiguous_multi там, где близких кандидатов нет.

### Прод-проверка после деплоя Проверял не «код смержен», а что механизм отвечает по-другому на живых данных. Запуск `find_core_matches` в работающем контейнере (только SELECT, наружу не ходит): ``` counts: {'tier_a': 3, 'tier_b': 31, 'ambiguous': 14, 'skipped_taken': 507} занятых имён в отчёте: 308 <- новое поле заполняется неоднозначных: 14 после отсева остался 1 кандидат: 14 -> гео-проход теперь может резолвить осталось несколько: 0 не осталось никого: 0 ``` До правки все 14 уходили в геокод парами, и половина вызовов DaData тратилась на кандидатов, которых запись отбрасывает по `ON CONFLICT`. Чего проверка **не** доказывает: ни одна из 14 пока не резолвлена на самом деле — для этого нужен запуск гео-прохода, а он ручной и тратит квоту DaData. Критерий приёмки на будущее: после следующего `run_geo_pass` строк с `match_method='auto_core_geo_v6'` должно стать больше 2, и в логе должны появиться reject'ы с причиной `too_far` вместо `ambiguous_multi` там, где близких кандидатов нет.
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#2929
No description provided.