gendesign/tradein-mvp/backend/app/services/matching/normalize.py
lekss361 89c4a2842d
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 43s
Deploy Trade-In / deploy (push) Successful in 22s
feat(tradein): cross-source matching service (3-tier: cadastr / fingerprint / geo / composite) (#470)
2026-05-23 14:12:16 +00:00

76 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Address normalization and fingerprint utilities.
Used by 3-tier matching to produce stable, source-independent keys.
Algorithm reference: decisions/Cross_Source_Matching_Strategy.md sec 3.2
"""
import hashlib
import re
import unicodedata
# Strip punctuation EXCEPT hyphens — keep hyphens so abbreviation rules like
# 'пр-кт', 'б-р', 'пр-д' can match before they are collapsed to spaces.
_PUNCT = re.compile(r'[^\w\s\-]', flags=re.UNICODE)
_WS = re.compile(r'\s+')
# Abbreviation expansion — applied after lowercasing, bounded by spaces.
# Order matters: longer/more-specific patterns first to avoid partial overlap.
# Hyphenated forms ('пр-кт', 'б-р', 'пр-д') come first so they match before
# the shorter variants ('пр', 'б') would erroneously consume the prefix.
_ABBREV = [
(' пр-кт ', ' проспект '),
(' пр-кт. ', ' проспект '),
(' б-р ', ' бульвар '),
(' пр-д ', ' проезд '),
(' ш ', ' шоссе '),
(' ул ', ' улица '),
(' ул. ', ' улица '),
(' пр ', ' проспект '),
(' пр. ', ' проспект '),
(' пер ', ' переулок '),
(' пл ', ' площадь '),
(' наб ', ' набережная '),
(' бул ', ' бульвар '),
(' туп ', ' тупик '),
(' стр ', ' строение '),
(' корп ', ' корпус '),
(' д ', ' дом '),
(' к ', ' корпус '),
]
def normalize_address(text: str | None) -> str:
"""Normalize address string for cross-source comparison.
Steps:
1. Unicode NFC normalization
2. Lowercase
3. Strip punctuation except hyphens (preserve 'пр-кт', 'б-р', 'пр-д' for expansion)
4. Collapse whitespace
5. Expand common Russian address abbreviations (hyphenated forms first)
6. Collapse remaining hyphens to spaces
"""
if not text:
return ""
s = unicodedata.normalize('NFC', text).lower()
s = _PUNCT.sub(' ', s) # strip punctuation EXCEPT hyphens
s = _WS.sub(' ', s).strip()
# Wrap with spaces for clean boundary matching
s = f' {s} '
for short, full in _ABBREV:
s = s.replace(short, full)
# Collapse remaining hyphens (e.g. standalone '-' separators or numeric ranges)
s = s.replace('-', ' ')
return _WS.sub(' ', s).strip()
def address_fingerprint(address: str | None, lat: float | None, lon: float | None) -> str:
"""SHA-256 fingerprint of normalized address + rounded coordinates (4 dp ≈ 11 m).
Returns first 32 hex chars of the digest (128 bits — collision-safe for millions of rows).
"""
norm_addr = normalize_address(address or '')
lat_r = f'{lat:.4f}' if lat is not None else ''
lon_r = f'{lon:.4f}' if lon is not None else ''
key = f'{norm_addr}|{lat_r}|{lon_r}'
return hashlib.sha256(key.encode('utf-8')).hexdigest()[:32]