"""Единый гейт правдоподобия для высоты потолков (#2699). Канон — колонка ``listings.ceiling_height_m`` (метры). Единицы вынесены в имя, как и во всём остальном коде проекта (``domrf_kn_flats.ceiling_height_m``, ``domrf_kn_objects.ceiling_height_m``, фронтовый ``ceiling_height_m``); ``listings.ceiling_height`` (миграция 019, numeric(3,2)) — единственное исключение, помеченное DEPRECATED в миграции 238. Гейт был только у yandex SERP (инлайн, #2007) — остальные писатели гнали в БД что распарсили. Прод 2026-08-07: в ``ceiling_height_m`` 26 строк > 6 м (максимум 29.90) и 84 строки < 2 м (83 из них — ровно 0.00), все из avito detail. Теперь гейт один на всех писателей. Диапазон 2.0–6.0 м: снизу — ниже жилой потолок не бывает (СП 54.13330 требует от 2.5 м; 2.0 оставлено с запасом на мис-парс округлений), сверху — сталинки и лофты редко переваливают за 5 м, 6.0 покрывает двухуровневые карточки, где продавец указывает высоту помещения целиком. Тот же диапазон уже год работает у yandex SERP; прод-подтверждение: все 7 675 yandex-значений лежат в [2.00, 6.00]. Второй эффект гейта: ``ceiling_height`` — numeric(3,2), max 9.99, и запись out-of-range роняла бы весь батч DataError'ом (per-lot SAVEPOINT ловит только IntegrityError). Гейт снимает и этот класс отказа. """ from __future__ import annotations CEILING_MIN_M = 2.0 CEILING_MAX_M = 6.0 def plausible_ceiling_m(value: float | int | str | None) -> float | None: """Вернуть высоту потолка в метрах, если она физически возможна, иначе None. Нечисловой вход (``None``, пустая строка, мусор) → ``None`` без исключения: вызывающие — парсеры чужого HTML/JSON, для них «не разобралось» это норма, а не ошибка. Сырое значение остаётся в ``raw_payload`` карточки. """ if value is None: return None try: height = float(value) except (TypeError, ValueError): return None if CEILING_MIN_M <= height <= CEILING_MAX_M: return height return None