From 7f8e0d2d2ec41cedc9c12c292a4bc1e3fa6b9535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Mon, 21 Sep 2026 22:22:22 +0300 Subject: [PATCH] feat: denylist streets named after people, fix adjectival surname declension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Богдана Хмельницкого" and "Великие Луки" (a real city, misread as the given name "Лука") were already handled by the ORGANISATION_NEARBY veto added earlier — but only when an organization word is nearby. A street name with no such word close by ("Живу на улице Карла Маркса") had no protection, since single-word commemorative streets ("улица Ленина") never matched the two-word FIO patterns to begin with — it's only the two-word ones (person's first + last name) that are at risk. Added ~24 of Russia's most common commemorative street-name surnames (Rosreestr's own ranking: Lenin is #1, Gagarin #2, Kirov #3, ...) to the existing well-known-people denylist, reusing the mechanism already built for Pushkin/Tolstoy. Testing it surfaced a real gap in Declension: surnames declining as an adjective ("Дзержинский" → "Дзержинского", "-ий" replaced by "-ого" wholesale, not extended) weren't matching at all in genitive case — and genitive is the ONLY case a street name is ever written in ("улица Дзержинского", never "улица Дзержинский"). Single-vowel stripping fixed regular surnames (Пушкин → Пушкина) but not this pattern. Now strips the whole "-ск-" ending down to the "ск" stem before falling back to the vowel-strip. Adjectives without "-ск-" ("Толстой" → "Толстого") remain a known, documented limitation. Co-Authored-By: Claude Sonnet 5 --- .../java/ru/pdguard/detect/Declension.java | 24 ++++- src/main/resources/names/well-known.txt | 31 +++++++ .../java/ru/pdguard/StreetDenylistTest.java | 93 +++++++++++++++++++ .../ru/pdguard/detect/NameDictionaryTest.java | 22 +++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/test/java/ru/pdguard/StreetDenylistTest.java diff --git a/src/main/java/ru/pdguard/detect/Declension.java b/src/main/java/ru/pdguard/detect/Declension.java index 03d1ed4..7de39c6 100644 --- a/src/main/java/ru/pdguard/detect/Declension.java +++ b/src/main/java/ru/pdguard/detect/Declension.java @@ -10,19 +10,41 @@ import java.util.Locale; * «Пушкин» → «Пушкина») — там основы из списка достаточно как есть. Слова на * гласную меняют последнюю букву («Москва» → «Москве», «Ольга» → «Ольге») — * для них сравнение идёт по основе без неё. + * + *

Фамилии на «-ский» склоняются как прилагательное: окончание меняется + * целиком («Дзержинский» → «Дзержинского», «-ий» на «-ого», а не дописывается), + * поэтому для них отсечения одной буквы недостаточно — основа обрезается сразу + * до «ск». Для улиц в честь людей это не редкий случай, а основной: «улица + * Дзержинского», «улица Островского» пишутся только в родительном падеже, + * именительный там не встречается вообще. */ final class Declension { + /** + * Падежные окончания прилагательного склонения на «-ск-»: мужской, женский + * и средний род, все падежи. Проверяются от длинных к коротким — «-ского» + * не должно потеряться из-за более короткого совпадения на «-ким» и т.п. + */ + private static final String[] ADJECTIVE_ENDINGS = { + "ского", "скому", "ским", "ском", "скую", "ской", "скою", "ская", "ский" + }; + private Declension() { } /** - * Отбрасывает у основы конечную гласную, которая меняется по падежам. + * Отбрасывает у основы окончание, которое меняется по падежам: гласную — + * у обычных слов, целиком «-ск-»-окончание — у прилагательных фамилий. * Слова короче четырёх букв не трогает — короткая основа и так шире * большинства падежных форм. */ static String withoutInflectedEnding(String word) { String lower = word.toLowerCase(Locale.ROOT); + for (String ending : ADJECTIVE_ENDINGS) { + if (lower.length() > ending.length() && lower.endsWith(ending)) { + return lower.substring(0, lower.length() - ending.length() + 2); + } + } if (lower.length() >= 4 && "аяйь".indexOf(lower.charAt(lower.length() - 1)) >= 0) { return lower.substring(0, lower.length() - 1); } diff --git a/src/main/resources/names/well-known.txt b/src/main/resources/names/well-known.txt index a6b95d3..4087339 100644 --- a/src/main/resources/names/well-known.txt +++ b/src/main/resources/names/well-known.txt @@ -59,3 +59,34 @@ Дерипаска Абрамович Усманов + +# Фамилии, в честь которых чаще всего называют улицы в России (Росреестр, +# Яндекс.Исследования). Упоминание «улица Ленина» само по себе не задевает +# распознавание ФИО — для него нужны два слова, — но составные названия +# («Феликса Дзержинского», «Александра Матросова») попадают под то же +# правило, что и «Богдана Хмельницкого»: в честь человека, а не клиент. +Ленин +Киров +Свердлов +Дзержинский +Фрунзе +Куйбышев +Ворошилов +Будённый +Буденный +Чапаев +Жуков +Чкалов +Терешкова +Мичурин +Шевченко +Орджоникидзе +Калинин +Энгельс +Маркс +Островский +Матросов +Волошина +Сусанин +Димитров +Донской diff --git a/src/test/java/ru/pdguard/StreetDenylistTest.java b/src/test/java/ru/pdguard/StreetDenylistTest.java new file mode 100644 index 0000000..8c0cb9a --- /dev/null +++ b/src/test/java/ru/pdguard/StreetDenylistTest.java @@ -0,0 +1,93 @@ +package ru.pdguard; + +import org.junit.jupiter.api.Test; +import ru.pdguard.config.SystemPolicy; +import ru.pdguard.core.PayloadStore; +import ru.pdguard.core.Pipeline; +import ru.pdguard.detect.RuleRegistry; +import ru.pdguard.mask.Masker; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Составные названия улиц в честь людей — тот же класс ложных срабатываний, + * что и «Богдана Хмельницкого» на реальных адресах Альфа-Банка: правило ФИО + * без ролевого слова ловит «имя + фамилия по словообразованию», а улица в + * честь исторической фигуры выглядит точно так же. Одиночная фамилия («улица + * Ленина») под это правило не подпадает вообще — ему нужны два слова, поэтому + * все примеры здесь двухсловные, реальные названия улиц. + */ +class StreetDenylistTest { + + private final Pipeline pipeline = + new Pipeline(new RuleRegistry(), new Masker(), new PayloadStore(1_000_000L, 30)); + + private String mask(String text) { + return pipeline.process(text, UUID.randomUUID().toString(), SystemPolicy.DEFAULT); + } + + private void assertUnmasked(String text) { + assertEquals(text, mask(text), "улица в честь исторической фигуры не должна маскироваться: " + text); + } + + @Test + void doesNotMaskNikolaiOstrovskyStreet() { + assertUnmasked("Живу на улице Николая Островского уже десять лет"); + } + + @Test + void doesNotMaskAlexanderMatrosovStreet() { + assertUnmasked("Магазин находится на улице Александра Матросова"); + } + + @Test + void doesNotMaskValeryChkalovStreet() { + assertUnmasked("Заезжайте на улицу Валерия Чкалова"); + } + + @Test + void doesNotMaskVeraVoloshinaStreet() { + assertUnmasked("Новый дом построили на улице Веры Волошиной"); + } + + @Test + void doesNotMaskIvanSusaninStreet() { + assertUnmasked("Школа расположена на улице Ивана Сусанина"); + } + + @Test + void doesNotMaskSergeyKirovStreet() { + assertUnmasked("Парковка есть на улице Сергея Кирова"); + } + + @Test + void doesNotMaskGeorgiDimitrovStreet() { + assertUnmasked("Остановка на улице Георгия Димитрова"); + } + + /** + * Тот же принцип, что и у Пушкина: рядом с другим ПД денилист не + * применяется — если в тексте всё-таки есть настоящие персональные данные, + * совпадение с историческим именем их не прикрывает. + */ + @Test + void masksCommemorativeStreetNameWhenOtherPersonalDataIsPresent() { + String masked = mask("Живу на улице Николая Островского, тел. +7 916 123-45-67"); + assertFalse(masked.contains("Николая Островского"), masked); + } + + /** Контроль: обычное клиентское имя того же грамматического вида всё ещё маскируется. */ + @Test + void stillMasksRealClientNameWithSimilarPattern() { + String masked = mask("Живёт на улице, зовут Николая Смирнова"); + assertFalse(masked.contains("Николая Смирнова"), masked); + } + + @Test + void doesNotMaskDmitryDonskoyStreet() { + assertUnmasked("Дом стоит на улице Дмитрия Донского"); + } +} diff --git a/src/test/java/ru/pdguard/detect/NameDictionaryTest.java b/src/test/java/ru/pdguard/detect/NameDictionaryTest.java index 61a6497..dede0b1 100644 --- a/src/test/java/ru/pdguard/detect/NameDictionaryTest.java +++ b/src/test/java/ru/pdguard/detect/NameDictionaryTest.java @@ -42,6 +42,28 @@ class NameDictionaryTest { assertFalse(NameDictionary.isWellKnown("Заявление Смирнова")); } + /** + * Фамилии, в честь которых чаще всего называют улицы в России (Росреестр). + * Само по себе «улица Ленина» никогда не попало бы под ФИО — для этого + * правила нужны два слова, — но денилист должен покрывать и составные + * названия («Феликса Дзержинского»), и вариации написания («Будённый»/ + * «Буденный»). + */ + @Test + void bundledListCoversCommemorativeStreetNames() { + assertTrue(NameDictionary.isWellKnown("улица Кирова")); + // «-ский» склоняется целиком («Дзержинский» → «Дзержинского»), для таких + // основа обрезается сразу до «ск» — см. Declension. + assertTrue(NameDictionary.isWellKnown("проспект Дзержинского")); + assertTrue(NameDictionary.isWellKnown("улица Донского")); + // «Будённый» — чистое прилагательное без «-ский» (как «Толстой»): та же + // известная граница приёма, родительный падеж («Будённого») им не ловится. + assertTrue(NameDictionary.isWellKnown("улица Будённый")); + assertTrue(NameDictionary.isWellKnown("улица Буденный"), "написание без «ё» тоже должно ловиться"); + assertTrue(NameDictionary.isWellKnown("улица Жукова")); + assertTrue(NameDictionary.isWellKnown("улица Островского")); + } + @Test void externalFileAddsNamesWithoutRebuild(@TempDir Path dir) throws IOException { Path file = dir.resolve("well-known.txt");