ПТИЦА: гео-радиусная цена участка считается по лотам проектов ближних ЖК, а не по чужим лотам под устаревшим complex_id (#3583) #3591

Merged
bot-backend merged 1 commit from fix/3583-radius-price-complex into main 2026-09-17 13:32:56 +00:00
Collaborator

#3583: гео-радиусная цена участка брала лоты чужих ЖК

Что было

analyze_parcelgeo_radius_price (медиана ₽/м² новостроек в 3 км; идёт в financial_estimate и district.median_price_basis, если нет квартальной MV) отбирал лоты так: complexes в радиусе → objective_lots WHERE complex_id IN (...). Та же колонка, что в #2962: проставлена один раз миграцией 76, UPSERT 70_parse_objective_raw.py переписывает project_name и не трогает complex_id. Прод 17.09: из 303 677 строк с complex_id у 236 354 проект чужой (цифра из #3582, не перемерял).

Улики (прод, только чтение, BEGIN READ ONLY — сам прод отбил CREATE TEMP TABLE в этой транзакции)

  1. Воспроизведение тем же путём. Центроид — тем же union cad_quarters_geom → cad_buildings → cad_parcels_geom + ST_Centroid, что в analyze_parcel, SQL — дословный текст из origin/main. Последний сохранённый прогон после загрузки Объектива 15.09: analysis_runs для 66:41:0702048:27 от 16.09 хранит geo_radius_price = (130552.097266025, 13638, 18), пересчёт старым SQL даёт то же самое до знака.
  2. Старая медиана не зависит от места. На 280 участках из случайной выборки старые медианы лежат в 125–138 тыс. (p10–p90), то есть по сути это средняя по городу. Новые: 107–181 тыс.
  3. Дефект жив везде, где есть лоты. Совпадений нет ни у одного участка с опубликованной медианой (таблица ниже).

Что сделано

  1. complex → проект через complex_sources (source='objective') (350 строк, строго 1:1), лоты по project_name. objective_lots.complex_id больше не читается.
  2. Сверка имени нужна и здесь (в issue вопрос был открыт). Сверяю так же, как в #2962, только с complexes.canonical_name, потому что объекта ДОМ.РФ тут нет. Из 350 связей отвергнуто 6, проверил по координатам и адресам лотов:
complex связь в complex_sources вердикт
1493 ЖК VEER PARK (Эльмаш) Clever Park (Парковый, ул. Машинная) неверная, 11.7 км
1437 ЖК Клевер парк Веер Парк (Эльмаш) неверная, 11.7 км
1352 ЖК Графит (56.90, 60.59) Гранит (ЖБИ, Высоцкого 1Г) неверная
1420 Жилой квартал "Традиции" (Эльмаш, Турбинка) Традиция (Втузгородок, Лодыгина 15а, это complex 642) неверная. В #3582 её сочли «вероятно, тем же ЖК», это не так
1097 ЖК Новая Ботаника Новая Ботаника-2 (Ботанический) верная, теряется
1148 ЖК Теплые кварталы Теплые кварталы (BAZA) (Втузгородок) верная, теряется

Итог: 4 чужих проекта в 4.5–11.7 км отсекаются, 2 своих теряются. Разбивка изменений «без сверки → со сверкой» по участкам: md5-выборка — 66 только из-за неверных связей, 30 только из-за верных, 36 из-за обеих, необъяснённых 0. Реальные участки: 13 / 4 / 11 / 0.
3. Скорость. Простая замена фильтра на JOIN ... ON project_name читала все снапшоты проектов (150 400 строк), и сортировка уходила на диск (external merge, 13 МБ): 690 мс против 67 мс у старого запроса. Поэтому дедуп физлота перенесён в LATERAL по проекту с premise_kind = 'квартира'. Так работает objective_lots_physflat_covering_v2_idx (Index Only Scan, без сортировки), и так же отбирают квартиры соседние читатели (market_metrics, best_layouts, _SOLD_COUNT_SQL). EXPLAIN ANALYZE на проде, по 3 прогона:

участок старый JOIN по project_name LATERAL без фильтра итог
66:41:0702048:27 (16 проектов) 66–68 мс 645–654 мс 468–473 мс 61–85 мс
66:41:0401048:109 (26 ЖК) 67–92 мс 691–717 мс 498–501 мс 65–116 мс

Фильтр по квартира на медиану не влияет: апартаменты (11 260 строк) есть только у 6 связанных проектов (Re:Volution Towers ×2, Башня «Исеть», Проспект Мира. Компаунд, Свобода Residence, Тактика), и у всех complex без координат, в радиус они не попадают. На 570 участках все три варианта (JOIN, LATERAL, LATERAL + квартира) совпали по (median, n, n_complexes) у 570 из 570.
4. SQL вынесен в _GEO_RADIUS_PRICE_SQL (как _COMPETITORS_SQL), чтобы тест гонял настоящий текст. Мок в test_analyze_market_price.py ищет "AS n_complexes" + "objective_lots", оба на месте.

v_complex_full.objective_lots_n (пункт 2 issue): не трогал, столбец никто не читает. grep по backend/app, tradein-mvp, frontend: v_complex_full читается только в analytics_queries.py:286 (district_name, cad_quarter) и :3052 (canonical_name, cad_quarter, latitude, longitude, cad_buildings_n). На проде зависимых view нет (pg_depend: 0 строк). Правка view — это миграция, её делает database-expert, если столбец когда-то начнут читать.

Замер «сколько прежних результатов изменится» (прод 17.09, старый SQL против закоммиченного текста константы, радиус 3 км)

Наборы: real — все 70 участков из analysis_runs; md5 — 500 участков из cad_parcels_geom, ORDER BY md5(cad_num) LIMIT 500. Опубликовано = гейт n ≥ 10 и ≥ 2 ЖК.

real (70) md5 (500)
с лотами в радиусе 58 307
без изменений 12, у всех нет лотов в радиусе 193, у всех нет лотов в радиусе
медиана опубликована, было → стало 55 → 55 281 → 280
прежних опубликованных без изменений 0 из 55 0 из 280
расхождение > 10 % / > 20 % 24 / 15 169 / 105
медиана / p90 / максимум расхождения 8.3 % / 25.1 % / 28.1 % 15.2 % / 24.8 % / 29.3 %
медиана выросла 45 из 55 213 из 280
разброс медиан p10–p90, было → стало 125.0–137.5 → 119.7–183.5 тыс. 125.4–138.1 → 107.3–180.9 тыс.

Гейт перевернулся у одного участка: 66:41:0105033:19 (126 219, 1388 лотов, 2 ЖК) → (115 000, 1305, 1). Второй ЖК там был Графит с лотами «Гранита» из ЖБИ, так что медиана честно не публикуется.

Примеры (real): 66:41:0401048:109 137 743 → 191 449; 66:41:0304011:1124 133 665 → 177 583; 66:41:0610029:83 127 963 → 96 747 (1 ЖК, не публикуется ни до, ни после); 66:41:0702048:27 130 552 → 138 880.

Сохранённые расчёты, которые на это опирались (analysis_runs, их не переписываю): медиана опубликована в 1435 прогонах по 18 участкам, financial_estimate.price_source = objective_geo_radius — в 381 прогоне (2 участка, последний 10.08), district.median_price_basis — в 380 (4 участка). С 17.08 прогонов 14 по 3 участкам.

Стабильность замера: старый SQL в двух проходах совпал у 570 из 570, закоммиченный текст совпал с первым проходом нового у 570 из 570.

Тест и фальсификация

backend/tests/api/v1/test_3583_geo_radius_price_project_bridge.py гоняет _GEO_RADIUS_PRICE_SQL на временных таблицах (образец — test_2962_competitors_gapfill_bridge.py). Под complex_id ближнего ЖК лежат 3 лота чужого «Малахита» (его complex в 10 км), у ближнего VEER PARK неверная связь на «Clever Park», «СтудияПарк» проверяет сверку без пробела, у одного лота два снапшота. Ожидание по значению: {'median': 100000.0, 'n': 5.0, 'n_complexes': 2.0}. Режимы как у #2962: в CI (PostGIS уже поднят после #3582) пропуск выключен, вне CI и без базы пропуск объявлен в skip_allowlist.txt (проверено: 1 skipped, rc=0).

Фальсификация: исправленный parcels.py скопирован в сторону, в файл по очереди подложены мутанты, после каждого прогона копия возвращена и сверена diff -q. git stash не использовался. Константа в мутантах остаётся, меняется только тело SQL, поэтому красный здесь значит «неверное значение», а не ImportError.

1) в константе SQL из origin/main (ol.complex_id IN nearby_cx)          rc=1
E         {'n_complexes': 1.0} != {'n_complexes': 2.0}
E         {'median': 300000.0} != {'median': 100000.0}
E         {'n': 4.0} != {'n': 5.0}
FAILED ...::test_median_counts_own_projects_of_nearby_complexes
2) снята сверка имени (эталон «по project_name» из issue)                rc=1
E         {'n_complexes': 3.0} != {'n_complexes': 2.0}
E         {'median': 115000.0} != {'median': 100000.0}
E         {'n': 8.0} != {'n': 5.0}
3) дедуп физлота: snapshot_date DESC → ASC                               rc=1
E         {'median': 95000.0} != {'median': 100000.0}

Прогоны (локально, postgis/postgis:16-3.4, CI=true TESTING=1, голова 7c52622a)

  • tests/api/v1/test_3583_geo_radius_price_project_bridge.py: 1 passed, rc=0.
  • tests/api/v1/test_analyze_market_price.py + новый тест без базы: 8 passed, 1 skipped (объявлен), rc=0.
  • Весь backend/tests: 5112 passed, 38 skipped, 11 deselected, 4 failed, rc=1 (код взят у самого pytest). Все 4 падения в tests/ops/test_2203_backup_trailer_grep_dashdash.py: mktemp: unrecognized option '--suffix=.sql.gz', BSD mktemp на macOS, то же, что в #3582. НЕУЧТЁННЫЙ ПРОПУСК в логе нет.
  • uv run ruff check app tests: All checks passed. ruff format --check по изменённым файлам: ok. pre-commit: passed.

Что не сделано

  • Сама колонка objective_lots.complex_id остаётся устаревшей: правка UPSERT и backfill ~3.2 млн строк — это запись на прод, решает владелец (как в #3582).
  • Fuzzy-связи complex_sources: 4 неверные и 2 верные, но непрошедшие сверку, не исправлял. Верные («Новая Ботаника-2», «Теплые кварталы (BAZA)») можно вернуть, только поправив данные связи или нормализацию имени. Второе разведёт сверку с #2962, поэтому не делал.
  • data/sql/80_complexes_backfill_nulls.sql — разовая миграция, на ответы не влияет.

Приёмка на проде (после деплоя, с 18.09.2026 и до следующей загрузки Объектива, ожидается 22.09 ~03:00 UTC; после загрузки ожидаемые числа ниже устаревают, и замер надо перезапускать)

  1. Код в контейнере: docker exec gendesign-backend-1 grep -c _GEO_RADIUS_PRICE_SQL /app/app/api/v1/parcels.py2. В main этой строки нет (0), так что маркер отличает версии.
  2. Живой ответ: POST /api/v1/parcels/66:41:0702048:27/analyzegeo_radius_price = median 138880.0, lots_count 13911, complexes_count 16 (сейчас 130552.10 / 13638 / 18). Или только чтением: первая запись analysis_runs для этого участка после деплоя.
  3. Повторить замер на тех же 570 участках текстом SQL из контейнера: отличий от столбца «стало» 0, у 55 + 280 опубликованных медиан расхождение со старым SQL не нулевое.

Issue не закрываю: пункты 1–2 закрыты кодом и проверкой, но приёмка на проде (п. 1–3 выше) после деплоя ещё не сделана.

Refs #3583, #2962

🤖 Generated with Claude Code

## #3583: гео-радиусная цена участка брала лоты чужих ЖК ### Что было `analyze_parcel` → `geo_radius_price` (медиана ₽/м² новостроек в 3 км; идёт в `financial_estimate` и `district.median_price_basis`, если нет квартальной MV) отбирал лоты так: `complexes` в радиусе → `objective_lots WHERE complex_id IN (...)`. Та же колонка, что в #2962: проставлена один раз миграцией 76, UPSERT `70_parse_objective_raw.py` переписывает `project_name` и не трогает `complex_id`. Прод 17.09: из 303 677 строк с `complex_id` у 236 354 проект чужой (цифра из #3582, не перемерял). ### Улики (прод, только чтение, `BEGIN READ ONLY` — сам прод отбил `CREATE TEMP TABLE` в этой транзакции) 1. **Воспроизведение тем же путём.** Центроид — тем же union `cad_quarters_geom → cad_buildings → cad_parcels_geom` + `ST_Centroid`, что в `analyze_parcel`, SQL — дословный текст из `origin/main`. Последний сохранённый прогон после загрузки Объектива 15.09: `analysis_runs` для `66:41:0702048:27` от 16.09 хранит `geo_radius_price = (130552.097266025, 13638, 18)`, пересчёт старым SQL даёт то же самое до знака. 2. **Старая медиана не зависит от места.** На 280 участках из случайной выборки старые медианы лежат в 125–138 тыс. (p10–p90), то есть по сути это средняя по городу. Новые: 107–181 тыс. 3. **Дефект жив везде, где есть лоты.** Совпадений нет ни у одного участка с опубликованной медианой (таблица ниже). ### Что сделано 1. `complex → проект` через `complex_sources (source='objective')` (350 строк, строго 1:1), лоты по `project_name`. `objective_lots.complex_id` больше не читается. 2. **Сверка имени нужна и здесь** (в issue вопрос был открыт). Сверяю так же, как в #2962, только с `complexes.canonical_name`, потому что объекта ДОМ.РФ тут нет. Из 350 связей отвергнуто 6, проверил по координатам и адресам лотов: | complex | связь в complex_sources | вердикт | |---|---|---| | 1493 ЖК VEER PARK (Эльмаш) | Clever Park (Парковый, ул. Машинная) | **неверная**, 11.7 км | | 1437 ЖК Клевер парк | Веер Парк (Эльмаш) | **неверная**, 11.7 км | | 1352 ЖК Графит (56.90, 60.59) | Гранит (ЖБИ, Высоцкого 1Г) | **неверная** | | 1420 Жилой квартал "Традиции" (Эльмаш, Турбинка) | Традиция (Втузгородок, Лодыгина 15а, это complex 642) | **неверная**. В #3582 её сочли «вероятно, тем же ЖК», это не так | | 1097 ЖК Новая Ботаника | Новая Ботаника-2 (Ботанический) | верная, теряется | | 1148 ЖК Теплые кварталы | Теплые кварталы (BAZA) (Втузгородок) | верная, теряется | Итог: 4 чужих проекта в 4.5–11.7 км отсекаются, 2 своих теряются. Разбивка изменений «без сверки → со сверкой» по участкам: md5-выборка — 66 только из-за неверных связей, 30 только из-за верных, 36 из-за обеих, **необъяснённых 0**. Реальные участки: 13 / 4 / 11 / 0. 3. **Скорость.** Простая замена фильтра на `JOIN ... ON project_name` читала все снапшоты проектов (150 400 строк), и сортировка уходила на диск (external merge, 13 МБ): **690 мс против 67 мс** у старого запроса. Поэтому дедуп физлота перенесён в `LATERAL` по проекту с `premise_kind = 'квартира'`. Так работает `objective_lots_physflat_covering_v2_idx` (Index Only Scan, без сортировки), и так же отбирают квартиры соседние читатели (`market_metrics`, `best_layouts`, `_SOLD_COUNT_SQL`). EXPLAIN ANALYZE на проде, по 3 прогона: | участок | старый | JOIN по project_name | LATERAL без фильтра | **итог** | |---|---|---|---|---| | 66:41:0702048:27 (16 проектов) | 66–68 мс | 645–654 мс | 468–473 мс | **61–85 мс** | | 66:41:0401048:109 (26 ЖК) | 67–92 мс | 691–717 мс | 498–501 мс | **65–116 мс** | Фильтр по `квартира` на медиану не влияет: апартаменты (11 260 строк) есть только у 6 связанных проектов (Re:Volution Towers ×2, Башня «Исеть», Проспект Мира. Компаунд, Свобода Residence, Тактика), и у всех complex **без координат**, в радиус они не попадают. На 570 участках все три варианта (JOIN, LATERAL, LATERAL + квартира) совпали по `(median, n, n_complexes)` у 570 из 570. 4. SQL вынесен в `_GEO_RADIUS_PRICE_SQL` (как `_COMPETITORS_SQL`), чтобы тест гонял настоящий текст. Мок в `test_analyze_market_price.py` ищет `"AS n_complexes"` + `"objective_lots"`, оба на месте. **`v_complex_full.objective_lots_n` (пункт 2 issue): не трогал, столбец никто не читает.** grep по `backend/app`, `tradein-mvp`, `frontend`: `v_complex_full` читается только в `analytics_queries.py:286` (`district_name`, `cad_quarter`) и `:3052` (`canonical_name`, `cad_quarter`, `latitude`, `longitude`, `cad_buildings_n`). На проде зависимых view нет (`pg_depend`: 0 строк). Правка view — это миграция, её делает database-expert, если столбец когда-то начнут читать. ### Замер «сколько прежних результатов изменится» (прод 17.09, старый SQL против закоммиченного текста константы, радиус 3 км) Наборы: **real** — все 70 участков из `analysis_runs`; **md5** — 500 участков из `cad_parcels_geom`, `ORDER BY md5(cad_num) LIMIT 500`. Опубликовано = гейт `n ≥ 10` и `≥ 2 ЖК`. | | real (70) | md5 (500) | |---|---|---| | с лотами в радиусе | 58 | 307 | | без изменений | 12, у всех **нет лотов в радиусе** | 193, у всех **нет лотов в радиусе** | | медиана опубликована, было → стало | 55 → 55 | 281 → 280 | | **прежних опубликованных без изменений** | **0 из 55** | **0 из 280** | | расхождение > 10 % / > 20 % | 24 / 15 | 169 / 105 | | медиана / p90 / максимум расхождения | 8.3 % / 25.1 % / 28.1 % | 15.2 % / 24.8 % / 29.3 % | | медиана выросла | 45 из 55 | 213 из 280 | | разброс медиан p10–p90, было → стало | 125.0–137.5 → 119.7–183.5 тыс. | 125.4–138.1 → 107.3–180.9 тыс. | Гейт перевернулся у одного участка: `66:41:0105033:19` `(126 219, 1388 лотов, 2 ЖК) → (115 000, 1305, 1)`. Второй ЖК там был Графит с лотами «Гранита» из ЖБИ, так что медиана честно не публикуется. Примеры (real): `66:41:0401048:109` 137 743 → 191 449; `66:41:0304011:1124` 133 665 → 177 583; `66:41:0610029:83` 127 963 → 96 747 (1 ЖК, не публикуется ни до, ни после); `66:41:0702048:27` 130 552 → 138 880. Сохранённые расчёты, которые на это опирались (`analysis_runs`, их не переписываю): медиана опубликована в 1435 прогонах по 18 участкам, `financial_estimate.price_source = objective_geo_radius` — в 381 прогоне (2 участка, последний 10.08), `district.median_price_basis` — в 380 (4 участка). С 17.08 прогонов 14 по 3 участкам. Стабильность замера: старый SQL в двух проходах совпал у 570 из 570, закоммиченный текст совпал с первым проходом нового у 570 из 570. ### Тест и фальсификация `backend/tests/api/v1/test_3583_geo_radius_price_project_bridge.py` гоняет `_GEO_RADIUS_PRICE_SQL` на временных таблицах (образец — `test_2962_competitors_gapfill_bridge.py`). Под `complex_id` ближнего ЖК лежат 3 лота чужого «Малахита» (его complex в 10 км), у ближнего VEER PARK неверная связь на «Clever Park», «СтудияПарк» проверяет сверку без пробела, у одного лота два снапшота. Ожидание по значению: `{'median': 100000.0, 'n': 5.0, 'n_complexes': 2.0}`. Режимы как у #2962: в CI (PostGIS уже поднят после #3582) пропуск выключен, вне CI и без базы пропуск объявлен в `skip_allowlist.txt` (проверено: `1 skipped`, rc=0). Фальсификация: исправленный `parcels.py` скопирован в сторону, в файл по очереди подложены мутанты, после каждого прогона копия возвращена и сверена `diff -q`. `git stash` не использовался. Константа в мутантах остаётся, меняется только тело SQL, поэтому красный здесь значит «неверное значение», а не ImportError. ``` 1) в константе SQL из origin/main (ol.complex_id IN nearby_cx) rc=1 E {'n_complexes': 1.0} != {'n_complexes': 2.0} E {'median': 300000.0} != {'median': 100000.0} E {'n': 4.0} != {'n': 5.0} FAILED ...::test_median_counts_own_projects_of_nearby_complexes 2) снята сверка имени (эталон «по project_name» из issue) rc=1 E {'n_complexes': 3.0} != {'n_complexes': 2.0} E {'median': 115000.0} != {'median': 100000.0} E {'n': 8.0} != {'n': 5.0} 3) дедуп физлота: snapshot_date DESC → ASC rc=1 E {'median': 95000.0} != {'median': 100000.0} ``` ### Прогоны (локально, `postgis/postgis:16-3.4`, `CI=true TESTING=1`, голова 7c52622a) - `tests/api/v1/test_3583_geo_radius_price_project_bridge.py`: **1 passed**, rc=0. - `tests/api/v1/test_analyze_market_price.py` + новый тест без базы: 8 passed, 1 skipped (объявлен), rc=0. - Весь `backend/tests`: **5112 passed, 38 skipped, 11 deselected, 4 failed**, rc=1 (код взят у самого pytest). Все 4 падения в `tests/ops/test_2203_backup_trailer_grep_dashdash.py`: `mktemp: unrecognized option '--suffix=.sql.gz'`, BSD mktemp на macOS, то же, что в #3582. `НЕУЧТЁННЫЙ ПРОПУСК` в логе нет. - `uv run ruff check app tests`: All checks passed. `ruff format --check` по изменённым файлам: ok. pre-commit: passed. ### Что не сделано - **Сама колонка `objective_lots.complex_id`** остаётся устаревшей: правка UPSERT и backfill ~3.2 млн строк — это запись на прод, решает владелец (как в #3582). - **Fuzzy-связи `complex_sources`**: 4 неверные и 2 верные, но непрошедшие сверку, не исправлял. Верные («Новая Ботаника-2», «Теплые кварталы (BAZA)») можно вернуть, только поправив данные связи или нормализацию имени. Второе разведёт сверку с #2962, поэтому не делал. - `data/sql/80_complexes_backfill_nulls.sql` — разовая миграция, на ответы не влияет. ### Приёмка на проде (после деплоя, **с 18.09.2026 и до следующей загрузки Объектива, ожидается 22.09 ~03:00 UTC**; после загрузки ожидаемые числа ниже устаревают, и замер надо перезапускать) 1. Код в контейнере: `docker exec gendesign-backend-1 grep -c _GEO_RADIUS_PRICE_SQL /app/app/api/v1/parcels.py` → `2`. В `main` этой строки нет (0), так что маркер отличает версии. 2. Живой ответ: `POST /api/v1/parcels/66:41:0702048:27/analyze` → `geo_radius_price` = `median 138880.0, lots_count 13911, complexes_count 16` (сейчас 130552.10 / 13638 / 18). Или только чтением: первая запись `analysis_runs` для этого участка после деплоя. 3. Повторить замер на тех же 570 участках текстом SQL из контейнера: отличий от столбца «стало» 0, у 55 + 280 опубликованных медиан расхождение со старым SQL не нулевое. Issue не закрываю: пункты 1–2 закрыты кодом и проверкой, но приёмка на проде (п. 1–3 выше) после деплоя ещё не сделана. Refs #3583, #2962 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-17 13:22:41 +00:00
fix(parcels): гео-радиусная цена участка берёт лоты проектов ближних ЖК, а не чужие под устаревшим complex_id (#3583)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 2m48s
CI / backend-tests (pull_request) Successful in 7m42s
7c52622a76
geo_radius_price отбирал лоты по objective_lots.complex_id. Эта колонка
проставлена один раз миграцией 76, а еженедельный UPSERT переписывает
project_name и не трогает complex_id, поэтому под id ближнего ЖК лежат лоты
ЖК из других районов. На проде медиана любого участка сходилась к средней по
городу: 125–138 тыс. ₽/м² при реальном разбросе 107–181 тыс.

Как в #2962: complex → проект через complex_sources (source='objective'),
лоты по project_name, имя проекта сверяется с именем ЖК. Сверка отсекает 4
неверные fuzzy-связи (VEER PARK/Clever Park, Клевер/Веер, Графит/Гранит,
Традиции/Традиция) и 2 верные («Новая Ботаника-2», «Теплые кварталы (BAZA)»).

Дедуп физлота перенесён в LATERAL по проекту с premise_kind='квартира',
чтобы работал покрывающий индекс: иначе запрос шёл 690 мс вместо 62.
Апартаменты сейчас есть только у проектов, чьи complexes без координат, так
что на медиану фильтр не влияет (570 участков, 0 отличий).

SQL вынесен в _GEO_RADIUS_PRICE_SQL, чтобы тест гонял его на временных таблицах.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 98f587add6 into main 2026-09-17 13:32:56 +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#3591
No description provided.