#774-2 Parts 2/3/4 (Part 1 UNIQUE/dedup deferred — cadnum data not flowing).
Part 2 — fingerprint<->alias sync: two scrapers with same address but coords
beyond the 4-decimal fingerprint rounding produced distinct fp -> duplicate
houses. Add Tier 2b lookup by normalized_address after Tier 2a (fingerprint)
misses, register the new fp via _insert_alias; alias upsert DO NOTHING ->
DO UPDATE SET fingerprint to keep alias synced.
Part 3 — collapse double-nested SAVEPOINT: removed redundant inner
db.begin_nested() around match_or_create_house inside _link_listing_to_house.
Outer per-row SAVEPOINT in save_listings already isolates a failing lot from
siblings and from the listings INSERT (per backend.md). BEHAVIOR NOTE: a
house-match failure now aborts the whole hook for that lot (incl. listing_source
link) atomically instead of partially committing it.
Part 4 — confidence/method wiring deferred to #774-3 (matcher not on ingestion
path; listing_sources columns exist, written as source_link=1.0).
Refs #849
Закрывает последнюю часть #769 (A1/A2/B/C/D в #798/#804). Finding #17:
bare-city адреса геокодились в city-центроид и сохранялись как точные lat/lon →
mislocated листинги участвовали в radius ST_DWithin-аналогах.
- 089: ALTER TABLE listings ADD COLUMN IF NOT EXISTS geo_precision text (idempot).
- ScrapedLot.geo_precision + проброс в save_listings.
- geocode_missing.py: geo_precision='city' через существующий _geocode_is_coarse.
- estimator _fetch_analogs Tier H+W: + AND (geo_precision IS DISTINCT FROM 'city').
NULL проходит (консервативно, сдвига до backfill нет). Pricing не тронут.
7 тестов; 270 passed, ruff clean.