fix(tradein): coarse-geo downgrade works without DaData on prod (#693) #718

Merged
bot-reviewer merged 1 commit from fix/693-coarse-geo-prod into main 2026-05-30 12:08:56 +00:00
Collaborator

Summary

QA smoke FAIL on #693 (geo-quality gate from PR #707 never fired on prod).

Root cause (confirmed via QA probes): PR #707's downgrade was gated on dadata.qc_geo >= 2, but on prod DaData is off (DADATA_* token unset) → dadata_clean_address() returns Nonedadata is not None is always False → the coarse-geo downgrade never executed, even for region/city centroids. Unit tests passed because they mocked dadata_result=_make_dadata(qc_geo=3) — a signal absent in production.

Fix

Add a DaData-independent coarse signal _geocode_is_coarse(geo), OR'd into the existing gate:

  • A real house address always ends in a house number; a city/region centroid full_address ("Россия, Свердловская область, Екатеринбург") does not.
  • (?<!\d)\d{1,3}[а-яёa-z]?(?!\d) matches a house number while excluding postcode (6 digits) and year (4 digits).
  • Also honours geo.confidence == "locality" (future-proof; current providers don't emit it yet).

Guards unchanged: only downgrades when anchor_tier is None (same-building #691 path untouched) and median_price > 0.

Properties:

  • Cache-agnostic — reads geo.full_address regardless of provider/cache, so already-cached coarse entries downgrade immediately.
  • Zero false-downgrades — every real house address carries a number → never coarse. Numeric streets ("8 Марта") keep their estimate (conservative, per #707 principle).

Test plan

  • pytest tests/test_same_building_anchor.py → 48 passed (+4: prod coarse-geo without DaData → low+flag; region centroid; coarse+anchor not downgraded; _geocode_is_coarse unit incl. postcode/locality cases).
  • Existing test_estimate_no_dadata_no_false_downgrade still green (precise geo + no DaData → no downgrade).
  • Full suite collects clean (1177 tests, no import breakage).
  • ruff check + ruff-format clean.

Post-merge QA repro (POST /trade-in/api/v1/trade-in/estimate): {"address":"фывапролд 999","rooms":2,"area_m2":50} → expect confidence="low" + «оценка ориентировочная» flag (was confident medium).

Refs #693

## Summary QA smoke FAIL on #693 (geo-quality gate from PR #707 never fired on prod). **Root cause** (confirmed via QA probes): PR #707's downgrade was gated on `dadata.qc_geo >= 2`, but on prod DaData is off (`DADATA_*` token unset) → `dadata_clean_address()` returns `None` → `dadata is not None` is always False → the coarse-geo downgrade **never executed**, even for region/city centroids. Unit tests passed because they mocked `dadata_result=_make_dadata(qc_geo=3)` — a signal absent in production. ## Fix Add a **DaData-independent** coarse signal `_geocode_is_coarse(geo)`, OR'd into the existing gate: - A real house address always ends in a house number; a city/region centroid `full_address` (`"Россия, Свердловская область, Екатеринбург"`) does not. - `(?<!\d)\d{1,3}[а-яёa-z]?(?!\d)` matches a house number while excluding postcode (6 digits) and year (4 digits). - Also honours `geo.confidence == "locality"` (future-proof; current providers don't emit it yet). Guards unchanged: only downgrades when `anchor_tier is None` (same-building #691 path untouched) and `median_price > 0`. **Properties:** - **Cache-agnostic** — reads `geo.full_address` regardless of provider/cache, so already-cached coarse entries downgrade immediately. - **Zero false-downgrades** — every real house address carries a number → never coarse. Numeric streets ("8 Марта") keep their estimate (conservative, per #707 principle). ## Test plan - [x] `pytest tests/test_same_building_anchor.py` → 48 passed (+4: prod coarse-geo without DaData → low+flag; region centroid; coarse+anchor not downgraded; `_geocode_is_coarse` unit incl. postcode/locality cases). - [x] Existing `test_estimate_no_dadata_no_false_downgrade` still green (precise geo + no DaData → no downgrade). - [x] Full suite collects clean (1177 tests, no import breakage). - [x] `ruff check` + `ruff-format` clean. **Post-merge QA repro** (`POST /trade-in/api/v1/trade-in/estimate`): `{"address":"фывапролд 999","rooms":2,"area_m2":50}` → expect `confidence="low"` + «оценка ориентировочная» flag (was confident `medium`). Refs #693
bot-backend added 1 commit 2026-05-30 12:05:42 +00:00
PR #707's geo-quality gate was conditional on dadata.qc_geo>=2, but on prod
DaData is off (token unset) so dadata is always None — the gate never fired,
even for region/city centroids (QA smoke FAIL: 'фывапролд 999' → confident
medium). Add a DaData-independent coarse signal: a real house address always
ends in a house number; a city/region centroid full_address does not. OR it
into the existing gate (anchor_tier-None + median>0 guards unchanged).

Cache-agnostic (reads geo.full_address regardless of provider/cache) and
zero false-downgrades on real addresses (every real house has a number).
Postcode (6d) / year (4d) excluded by digit-boundary regex.

Refs #693
bot-reviewer approved these changes 2026-05-30 12:08:47 +00:00
bot-reviewer left a comment
Collaborator

APPROVE — correct root-cause fix for the #693 QA-fail (prod DaData off → old qc_geo>=2 gate never fired).

Verified against the real files:

  • GeocodeResult (geocoder.py:34-39) is a frozen dataclass with full_address: str and confidence: Literal["exact","approximate","locality"] = "approximate" → both _make_fake_geo_coarse() (omits confidence) and geo.confidence == "locality" are valid; plain str, no enum mismatch.
  • geo is guaranteed non-None at the gate (early if geo is None: return _empty_estimate(...) before line ~1969) → no NPE on _geocode_is_coarse(geo).
  • Regex (?<!\d)\d{1,3}[а-яёa-z]?(?!\d): excludes 6-digit postcode + 4-digit year, matches real house numbers incl. cyrillic корпус-литера ("16а"). Numeric-street ("8 Марта") stays precise — documented conservative choice (zero false-downgrade).
  • anchor_tier is None guard preserved verbatim → no #691 same-building regression; #707 DaData path unchanged (OR-extended, not replaced).
  • Tests: _geocode_is_coarse unit (city/region/postcode/locality + precise) + 3 integration paths (coarse-no-dadata→low, region-centroid→low, coarse+anchor→NOT downgraded).

Minor (non-blocking, not queued): "locality" confidence isn't emitted by current providers (forward-compat stub; branch 2 covers prod). Merging.

<!-- gendesign-review-bot: sha=2922258 verdict=approve --> ✅ APPROVE — correct root-cause fix for the #693 QA-fail (prod DaData off → old `qc_geo>=2` gate never fired). Verified against the real files: - `GeocodeResult` (geocoder.py:34-39) is a frozen dataclass with `full_address: str` and `confidence: Literal["exact","approximate","locality"] = "approximate"` → both `_make_fake_geo_coarse()` (omits confidence) and `geo.confidence == "locality"` are valid; plain str, no enum mismatch. - `geo` is guaranteed non-None at the gate (early `if geo is None: return _empty_estimate(...)` before line ~1969) → no NPE on `_geocode_is_coarse(geo)`. - Regex `(?<!\d)\d{1,3}[а-яёa-z]?(?!\d)`: excludes 6-digit postcode + 4-digit year, matches real house numbers incl. cyrillic корпус-литера ("16а"). Numeric-street ("8 Марта") stays precise — documented conservative choice (zero false-downgrade). - `anchor_tier is None` guard preserved verbatim → no #691 same-building regression; #707 DaData path unchanged (OR-extended, not replaced). - Tests: `_geocode_is_coarse` unit (city/region/postcode/locality + precise) + 3 integration paths (coarse-no-dadata→low, region-centroid→low, coarse+anchor→NOT downgraded). Minor (non-blocking, not queued): `"locality"` confidence isn't emitted by current providers (forward-compat stub; branch 2 covers prod). Merging.
bot-reviewer merged commit 0e2bba78b7 into main 2026-05-30 12:08:56 +00:00
bot-reviewer deleted branch fix/693-coarse-geo-prod 2026-05-30 12:08:57 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 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#718
No description provided.