"""Layout signature extraction для Issue #113 (Phase 2.1 minimal). Без `layout_type` / `balcony_count` — ждут B2B Объектив (#52). """ from __future__ import annotations from typing import Literal RoomBucket = Literal["studio", "euro-1", "euro-2", "1", "2", "3", "4+"] AreaBin = Literal["<25", "25-40", "40-60", "60-80", "80-100", "100+"] def room_bucket_from_flat( rooms: int | None, flat_type: str | None, is_studio: bool | None, total_area: float | None = None, ) -> RoomBucket: """Determine room_bucket из kn_flats полей. Priority: 1. is_studio=True OR flat_type='Квартира-студия' → "studio" 2. rooms=0 (без is_studio) → "studio" 3. Fix SF-08: rooms=2 + area<35 → "euro-1" (DOM.РФ маркирует малогабаритные как 2-комн) 4. Fix SF-08: rooms=2 + area<50 → "euro-2" (евро-двушки 35-50м²) 5. rooms IN (1, 2, 3) → str(rooms) 6. rooms >= 4 → "4+" 7. fallback на "1" (rooms is None и не studio) """ if is_studio is True or flat_type == "Квартира-студия": return "studio" if rooms is None: return "1" if rooms == 0: return "studio" if rooms == 2 and total_area is not None: if total_area < 35.0: return "euro-1" if total_area < 50.0: return "euro-2" if rooms >= 4: return "4+" if rooms in (1, 2, 3): return str(rooms) # type: ignore[return-value] return "1" def area_bin(area_m2: float) -> AreaBin: """Bucket площади per ARN-buckets (НСПД).""" if area_m2 < 25.0: return "<25" if area_m2 < 40.0: return "25-40" if area_m2 < 60.0: return "40-60" if area_m2 < 80.0: return "60-80" if area_m2 < 100.0: return "80-100" return "100+" def layout_signature(room_bucket_val: RoomBucket, area_bin_val: AreaBin) -> str: """Deterministic string-signature для группировки.""" return f"{room_bucket_val}__{area_bin_val}"