diff --git a/backend/app/services/scrapers/page_reservation_parser.py b/backend/app/services/scrapers/page_reservation_parser.py index 9797acc5..96ab63e7 100644 --- a/backend/app/services/scrapers/page_reservation_parser.py +++ b/backend/app/services/scrapers/page_reservation_parser.py @@ -127,10 +127,24 @@ def _parse_act_date(text: str) -> date | None: def _detect_kind(text: str, default_kind: str) -> str: - """Определяет тип операции: 'резервирование' | 'изъятие' | default_kind.""" - if _RE_REZERV.search(text): + """Определяет тип операции: 'резервирование' | 'изъятие' | default_kind. + + Побеждает то слово, что встретилось РАНЬШЕ, а не то, что стоит выше в + коде (#2464). Прежний безусловный приоритет «резервир» переклассифицировал + ВЕСЬ документ — все участки разом, — если постановление об изъятии хоть + раз ссылалось на резервирование (типовая формулировка «ранее + зарезервированных земель», ссылка на утративший силу акт). Тема документа + стоит в заголовке, поэтому позиция первого упоминания — сигнал сильнее + порядка проверок, и он симметричен: заголовок «О резервировании» так же + выигрывает у «изъятия» в теле. + """ + m_rez = _RE_REZERV.search(text) + m_izy = _RE_IZYAT.search(text) + if m_rez and m_izy: + return "резервирование" if m_rez.start() < m_izy.start() else "изъятие" + if m_rez: return "резервирование" - if _RE_IZYAT.search(text): + if m_izy: return "изъятие" return default_kind diff --git a/backend/tests/services/scrapers/test_2464_detect_kind_position.py b/backend/tests/services/scrapers/test_2464_detect_kind_position.py new file mode 100644 index 00000000..647ea28f --- /dev/null +++ b/backend/tests/services/scrapers/test_2464_detect_kind_position.py @@ -0,0 +1,83 @@ +"""Тип постановления определяется по первому упоминанию, а не по порядку проверок (#2464). + +`_detect_kind` проверял «резервир» ПЕРВЫМ и возвращал «резервирование» безусловно. +Постановление об изъятии, где резервирование упомянуто вскользь — типовая +формулировка «ранее зарезервированных земель», ссылка на утративший силу акт, — +переклассифицировало **весь документ**: `kind` в `extract_reservations` один на +все участки, поэтому ошибка размножается на каждую строку `land_reservation`. + +Побеждает то слово, что встретилось раньше. Тема документа стоит в заголовке, +поэтому позиция — сигнал сильнее порядка проверок, и он симметричен: заголовок +«О резервировании» так же выигрывает у «изъятия» в теле. + +Формулировки взяты с прода: `basis_act` в `land_reservation` (источник +`izyatie_ekb_ocr`, 27 строк, все «изъятие») начинается со слов «Сообщение о +планируемом изъятии земельных участков…». +""" + +from __future__ import annotations + +from app.services.scrapers.page_reservation_parser import ( + _detect_kind, + extract_reservations, +) + +# Заголовок — с прода, дословно. +_ЗАГОЛОВОК_ИЗЪЯТИЕ = ( + "Сообщение о планируемом изъятии земельных участков и объектов недвижимого " + "имущества для муниципальных нужд в целях строительства улицы Энергостроителей" +) + + +def test_izyatie_in_title_wins_over_later_rezervirovanie() -> None: + """Головной: упоминание резервирования в теле не переписывает тему заголовка. + + На origin/main возвращается «резервирование» — неверное значение, а не отказ. + """ + text = ( + f"{_ЗАГОЛОВОК_ИЗЪЯТИЕ}. Изъятию подлежат участки, ранее зарезервированные " + "постановлением о резервировании земель от 12.03.2019." + ) + assert ( + _detect_kind(text, "изъятие") == "изъятие" + ), "документ об изъятии классифицирован по случайному упоминанию в теле" + + +def test_all_parcels_of_the_document_are_affected() -> None: + """Ошибка типа размножается на КАЖДЫЙ участок — kind в документе один на всех.""" + text = ( + f"{_ЗАГОЛОВОК_ИЗЪЯТИЕ}. Изъять земельные участки с кадастровыми номерами " + "66:41:0303004:22, 66:41:0303004:23, 66:41:0206032:8499, ранее " + "зарезервированные постановлением о резервировании земель." + ) + records = extract_reservations(text, default_kind="изъятие") + assert len(records) == 3, f"разобрано участков: {len(records)}" + kinds = {r.reservation_kind for r in records} + assert kinds == {"изъятие"}, f"типы участков: {kinds} — ошибка ушла бы во все строки" + + +def test_rezervirovanie_in_title_still_wins() -> None: + """Контроль симметрии: заголовок о резервировании выигрывает у изъятия в теле. + + Ловит «починку» через простую смену порядка проверок — она сломала бы этот случай. + """ + text = ( + "Постановление о резервировании земель для муниципальных нужд. " + "Зарезервированные участки не подлежат изъятию до истечения срока." + ) + assert _detect_kind(text, "изъятие") == "резервирование" + + +def test_single_keyword_documents_unchanged() -> None: + """Контроль: документы с одним словом-маркером распознаются как раньше.""" + assert _detect_kind("Об изъятии земельного участка", "резервирование") == "изъятие" + assert _detect_kind("О резервировании земель", "изъятие") == "резервирование" + + +def test_no_keyword_falls_back_to_default() -> None: + """Контроль: без маркеров остаётся default_kind, а не выдуманный тип.""" + assert _detect_kind("Об утверждении схемы расположения участка", "изъятие") == "изъятие" + assert ( + _detect_kind("Об утверждении схемы расположения участка", "резервирование") + == "резервирование" + )