diff --git a/src/main/java/ru/pdguard/detect/RuleRegistry.java b/src/main/java/ru/pdguard/detect/RuleRegistry.java index 9e220d1..123fc1b 100644 --- a/src/main/java/ru/pdguard/detect/RuleRegistry.java +++ b/src/main/java/ru/pdguard/detect/RuleRegistry.java @@ -317,6 +317,7 @@ public class RuleRegistry { Rule.of(ADDRESS_CITY, "(?iu:\\bг\\.|\\bгор\\.|\\bгород)\\s?(\\p{Lu}[\\p{L}-]{1,30})\\b", 73) .groups(1) + .validatedBy(ToponymDictionary::isKnownCity) .vetoedBy(ORGANISATION_NEARBY) .anchoredBy("г.", "гор", "город"), diff --git a/src/main/java/ru/pdguard/detect/ToponymDictionary.java b/src/main/java/ru/pdguard/detect/ToponymDictionary.java new file mode 100644 index 0000000..7b8c9e8 --- /dev/null +++ b/src/main/java/ru/pdguard/detect/ToponymDictionary.java @@ -0,0 +1,81 @@ +package ru.pdguard.detect; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +/** + * Словарь городов России — проверка того, что значение, пойманное правилом + * {@code ADDRESS_CITY}, действительно похоже на существующий город, а не на + * произвольное слово с заглавной буквы после «г.». + * + *

Сравнение по началу слова, а не точным совпадением: падежные окончания + * («в Москве», «из Казани») тем самым покрываются без отдельного разбора + * морфологии, как и у известных людей в {@link NameDictionary}. + */ +public final class ToponymDictionary { + + private static final List CITY_STEMS = load("/names/cities.txt").stream() + .map(ToponymDictionary::withoutInflectedEnding) + .distinct() + .sorted(Comparator.comparingInt(String::length).reversed()) + .toList(); + + private ToponymDictionary() { + } + + /** + * Похоже ли значение на название города из словаря в любом падеже. + * + *

Города на согласную склоняются добавлением окончания («Тамбов» → + * «Тамбове»), поэтому начало слова из словаря — уже достаточный признак. + * Города на гласную меняют последнюю букву («Москва» → «Москве»), для + * них сравнение идёт по основе без неё — так же, как с личными именами + * в {@link NameDictionary}. + */ + public static boolean isKnownCity(String value) { + String lower = value.strip().toLowerCase(Locale.ROOT); + if (lower.isEmpty()) { + return false; + } + for (String stem : CITY_STEMS) { + if (lower.startsWith(stem)) { + return true; + } + } + return false; + } + + private static String withoutInflectedEnding(String name) { + String lower = name.toLowerCase(Locale.ROOT); + if (lower.length() >= 4 && "аяйь".indexOf(lower.charAt(lower.length() - 1)) >= 0) { + return lower.substring(0, lower.length() - 1); + } + return lower; + } + + private static List load(String resource) { + try (InputStream in = ToponymDictionary.class.getResourceAsStream(resource)) { + if (in == null) { + throw new IllegalStateException("Словарь не найден в сборке: " + resource); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + return reader.lines() + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .distinct() + // Длинные основы — раньше: «Санкт-Петербург» не должен проигрывать «Санкт». + .sorted(Comparator.comparingInt(String::length).reversed()) + .toList(); + } + } catch (IOException e) { + throw new UncheckedIOException("Не удалось прочитать словарь " + resource, e); + } + } +} diff --git a/src/main/resources/names/cities.txt b/src/main/resources/names/cities.txt new file mode 100644 index 0000000..acc1f7e --- /dev/null +++ b/src/main/resources/names/cities.txt @@ -0,0 +1,1128 @@ +# Города России (1111 названий): epogrebnyak/pensnarik open dataset, +# github.com/pensnarik/russian-cities. Проверка по началу слова — падежи +# покрываются основой, как и у известных людей в well-known.txt. +# +# Столицы СНГ и сопредельных стран — банк работает не только с российскими +# адресами, а датасет выше охватывает только города РФ. +Минск +Астана +Алматы +Бишкек +Ереван +Ташкент +Баку +Киев +Кишинёв +Душанбе +Тбилиси +Абаза +Абакан +Абдулино +Абинск +Агидель +Агрыз +Адыгейск +Азнакаево +Азов +Ак-Довурак +Аксай +Алагир +Алапаевск +Алатырь +Алдан +Алейск +Александров +Александровск +Александровск-Сахалинский +Алексеевка +Алексин +Алзамай +Алупка +Алушта +Альметьевск +Амурск +Анадырь +Анапа +Ангарск +Андреаполь +Анжеро-Судженск +Анива +Апатиты +Апрелевка +Апшеронск +Арамиль +Аргун +Ардатов +Ардон +Арзамас +Аркадак +Армавир +Армянск +Арсеньев +Арск +Артём +Артёмовск +Артёмовский +Архангельск +Асбест +Асино +Астрахань +Аткарск +Ахтубинск +Ачинск +Ачхой-Мартан +Аша +Бабаево +Бабушкин +Бавлы +Багратионовск +Байкальск +Баймак +Бакал +Баксан +Балабаново +Балаклава +Балаково +Балахна +Балашиха +Балашов +Балей +Балтийск +Барабинск +Барнаул +Барыш +Батайск +Бахчисарай +Бежецк +Белая Калитва +Белая Холуница +Белгород +Белебей +Белинский +Белово +Белогорск +Белозерск +Белокуриха +Беломорск +Белоозёрский +Белорецк +Белореченск +Белоусово +Белоярский +Белый +Белёв +Бердск +Березники +Берёзовский +Беслан +Бийск +Бикин +Билибино +Биробиджан +Бирск +Бирюсинск +Бирюч +Благовещенск +Благодарный +Бобров +Богданович +Богородицк +Богородск +Боготол +Богучар +Бодайбо +Бокситогорск +Болгар +Бологое +Болотное +Болохово +Болхов +Большой Камень +Бор +Борзя +Борисоглебск +Боровичи +Боровск +Бородино +Братск +Бронницы +Брянск +Бугульма +Бугуруслан +Будённовск +Бузулук +Буинск +Буй +Буйнакск +Бутурлиновка +Валдай +Валуйки +Велиж +Великие Луки +Великий Новгород +Великий Устюг +Вельск +Венёв +Верещагино +Верея +Верхнеуральск +Верхний Тагил +Верхний Уфалей +Верхняя Пышма +Верхняя Салда +Верхняя Тура +Верхотурье +Верхоянск +Весьегонск +Ветлуга +Видное +Вилюйск +Вилючинск +Вихоревка +Вичуга +Владивосток +Владикавказ +Владимир +Волгоград +Волгодонск +Волгореченск +Волжск +Волжский +Вологда +Володарск +Волоколамск +Волосово +Волхов +Волчанск +Вольск +Воркута +Воронеж +Ворсма +Воскресенск +Воткинск +Всеволожск +Вуктыл +Выборг +Выкса +Высоковск +Высоцк +Вытегра +Вышний Волочёк +Вяземский +Вязники +Вязьма +Вятские Поляны +Гаврилов Посад +Гаврилов-Ям +Гагарин +Гаджиево +Гай +Галич +Гатчина +Гвардейск +Гдов +Геленджик +Георгиевск +Глазов +Голицыно +Горбатов +Горно-Алтайск +Горнозаводск +Горняк +Городец +Городище +Городовиковск +Гороховец +Горячий Ключ +Грайворон +Гремячинск +Грозный +Грязи +Грязовец +Губаха +Губкин +Губкинский +Гудермес +Гуково +Гулькевичи +Гурьевск +Гусев +Гусиноозёрск +Гусь-Хрустальный +Давлеканово +Дагестанские Огни +Далматово +Дальнегорск +Дальнереченск +Данилов +Данков +Дегтярск +Дедовск +Демидов +Дербент +Десногорск +Джанкой +Дзержинск +Дзержинский +Дивногорск +Дигора +Димитровград +Дмитриев +Дмитров +Дмитровск +Дно +Добрянка +Долгопрудный +Долинск +Домодедово +Донецк +Донской +Дорогобуж +Дрезна +Дубна +Дубовка +Дудинка +Духовщина +Дюртюли +Дятьково +Евпатория +Егорьевск +Ейск +Екатеринбург +Елабуга +Елец +Елизово +Ельня +Еманжелинск +Емва +Енисейск +Ермолино +Ершов +Ессентуки +Ефремов +Железноводск +Железногорск +Железногорск-Илимский +Жердевка +Жигулёвск +Жиздра +Жирновск +Жуков +Жуковка +Жуковский +Завитинск +Заводоуковск +Заволжск +Заволжье +Задонск +Заинск +Закаменск +Заозёрный +Заозёрск +Западная Двина +Заполярный +Зарайск +Заречный +Заринск +Звенигово +Звенигород +Зверево +Зеленогорск +Зеленоград +Зеленоградск +Зеленодольск +Зеленокумск +Зерноград +Зея +Зима +Златоуст +Злынка +Змеиногорск +Знаменск +Зубцов +Зуевка +Ивангород +Иваново +Ивантеевка +Ивдель +Игарка +Ижевск +Избербаш +Изобильный +Иланский +Инза +Инкерман +Иннополис +Инсар +Инта +Ипатово +Ирбит +Иркутск +Исилькуль +Искитим +Истра +Ишим +Ишимбай +Йошкар-Ола +Кадников +Казань +Калач +Калач-на-Дону +Калачинск +Калининград +Калининск +Калтан +Калуга +Калязин +Камбарка +Каменка +Каменногорск +Каменск-Уральский +Каменск-Шахтинский +Камень-на-Оби +Камешково +Камызяк +Камышин +Камышлов +Канаш +Кандалакша +Канск +Карабаново +Карабаш +Карабулак +Карасук +Карачаевск +Карачев +Каргат +Каргополь +Карпинск +Карталы +Касимов +Касли +Каспийск +Катав-Ивановск +Катайск +Качканар +Кашин +Кашира +Кедровый +Кемерово +Кемь +Керчь +Кизел +Кизилюрт +Кизляр +Кимовск +Кимры +Кингисепп +Кинель +Кинешма +Киреевск +Киренск +Киржач +Кириллов +Кириши +Киров +Кировград +Кирово-Чепецк +Кировск +Кирс +Кирсанов +Киселёвск +Кисловодск +Клин +Клинцы +Княгинино +Ковдор +Ковров +Ковылкино +Когалым +Кодинск +Козельск +Козловка +Козьмодемьянск +Кола +Кологрив +Коломна +Колпашево +Колпино +Колтуши +Кольчугино +Коммунар +Комсомольск +Комсомольск-на-Амуре +Конаково +Кондопога +Кондрово +Константиновск +Копейск +Кораблино +Кореновск +Коркино +Королёв +Короча +Корсаков +Коряжма +Костерёво +Костомукша +Кострома +Котельники +Котельниково +Котельнич +Котлас +Котово +Котовск +Кохма +Красавино +Красноармейск +Красновишерск +Красногорск +Краснодар +Красное Село +Краснозаводск +Краснознаменск +Краснокаменск +Краснокамск +Красноперекопск +Краснослободск +Краснотурьинск +Красноуральск +Красноуфимск +Красноярск +Красный Кут +Красный Сулин +Красный Холм +Кремёнки +Кронштадт +Кропоткин +Крымск +Кстово +Кубинка +Кувандык +Кувшиново +Кудрово +Кудымкар +Кузнецк +Куйбышев +Кукмор +Кулебаки +Кумертау +Кунгур +Купино +Курган +Курганинск +Курильск +Курлово +Куровское +Курск +Куртамыш +Курчалой +Курчатов +Куса +Кушва +Кызыл +Кыштым +Кяхта +Лабинск +Лабытнанги +Лагань +Ладушкин +Лаишево +Лакинск +Лангепас +Лахденпохья +Лебедянь +Лениногорск +Ленинск +Ленинск-Кузнецкий +Ленск +Лермонтов +Лесной +Лесозаводск +Лесосибирск +Ливны +Ликино-Дулёво +Липецк +Липки +Лиски +Лихославль +Лобня +Лодейное Поле +Ломоносов +Лосино-Петровский +Луга +Луза +Лукоянов +Луховицы +Лысково +Лысьва +Лыткарино +Льгов +Любань +Люберцы +Любим +Людиново +Лянтор +Магадан +Магас +Магнитогорск +Майкоп +Майский +Макаров +Макарьев +Макушино +Малая Вишера +Малгобек +Малмыж +Малоархангельск +Малоярославец +Мамадыш +Мамоново +Мантурово +Мариинск +Мариинский Посад +Маркс +Махачкала +Мглин +Мегион +Медвежьегорск +Медногорск +Медынь +Межгорье +Междуреченск +Мезень +Меленки +Мелеуз +Менделеевск +Мензелинск +Мещовск +Миасс +Микунь +Миллерово +Минеральные Воды +Минусинск +Миньяр +Мирный +Михайлов +Михайловка +Михайловск +Мичуринск +Могоча +Можайск +Можга +Моздок +Мончегорск +Морозовск +Моршанск +Мосальск +Москва +Московский +Муравленко +Мураши +Мурино +Мурманск +Муром +Мценск +Мыски +Мытищи +Мышкин +Набережные Челны +Навашино +Наволоки +Надым +Назарово +Назрань +Называевск +Нальчик +Нариманов +Наро-Фоминск +Нарткала +Нарьян-Мар +Находка +Невель +Невельск +Невинномысск +Невьянск +Нелидово +Неман +Нерехта +Нерчинск +Нерюнгри +Нестеров +Нефтегорск +Нефтекамск +Нефтекумск +Нефтеюганск +Нея +Нижневартовск +Нижнекамск +Нижнеудинск +Нижние Серги +Нижний Ломов +Нижний Новгород +Нижний Тагил +Нижняя Салда +Нижняя Тура +Николаевск +Николаевск-на-Амуре +Никольск +Никольское +Новая Ладога +Новая Ляля +Новоалександровск +Новоалтайск +Новоаннинский +Нововоронеж +Новодвинск +Новозыбков +Новокубанск +Новокузнецк +Новокуйбышевск +Новомичуринск +Новомосковск +Новопавловск +Новоржев +Новороссийск +Новосибирск +Новосиль +Новосокольники +Новотроицк +Новоузенск +Новоульяновск +Новоуральск +Новохопёрск +Новочебоксарск +Новочеркасск +Новошахтинск +Новый Оскол +Новый Уренгой +Ногинск +Нолинск +Норильск +Ноябрьск +Нурлат +Нытва +Нюрба +Нягань +Нязепетровск +Няндома +Облучье +Обнинск +Обоянь +Обь +Одинцово +Озёрск +Озёры +Октябрьск +Октябрьский +Окуловка +Оленегорск +Олонец +Олёкминск +Омск +Омутнинск +Онега +Опочка +Оренбург +Орехово-Зуево +Орлов +Орск +Орёл +Оса +Осинники +Осташков +Остров +Островной +Острогожск +Отрадное +Отрадный +Оха +Оханск +Очёр +Павлово +Павловск +Павловский Посад +Палласовка +Партизанск +Певек +Пенза +Первомайск +Первоуральск +Перевоз +Пересвет +Переславль-Залесский +Пермь +Пестово +Петергоф +Петров Вал +Петровск +Петровск-Забайкальский +Петрозаводск +Петропавловск-Камчатский +Петухово +Петушки +Печора +Печоры +Пикалёво +Пионерский +Питкяранта +Плавск +Пласт +Плёс +Поворино +Подольск +Подпорожье +Покачи +Покров +Покровск +Полевской +Полесск +Полысаево +Полярные Зори +Полярный +Поронайск +Порхов +Похвистнево +Почеп +Починок +Пошехонье +Правдинск +Приволжск +Приморск +Приморско-Ахтарск +Приозерск +Прокопьевск +Пролетарск +Протвино +Прохладный +Псков +Пугачёв +Пудож +Пустошка +Пучеж +Пушкин +Пушкино +Пущино +Пыталово +Пыть-Ях +Пятигорск +Радужный +Райчихинск +Раменское +Рассказово +Ревда +Реж +Реутов +Ржев +Родники +Рославль +Россошь +Ростов +Ростов-на-Дону +Рошаль +Ртищево +Рубцовск +Рудня +Руза +Рузаевка +Рыбинск +Рыбное +Рыльск +Ряжск +Рязань +Саки +Салават +Салаир +Салехард +Сальск +Самара +Санкт-Петербург +Саранск +Сарапул +Саратов +Саров +Сасово +Сатка +Сафоново +Саяногорск +Саянск +Светлогорск +Светлоград +Светлый +Светогорск +Свирск +Свободный +Себеж +Севастополь +Северо-Курильск +Северобайкальск +Северодвинск +Североморск +Североуральск +Северск +Севск +Сегежа +Сельцо +Семикаракорск +Семилуки +Семёнов +Сенгилей +Серафимович +Сергач +Сергиев Посад +Сердобск +Серов +Серпухов +Сертолово +Сестрорецк +Сибай +Сим +Симферополь +Сковородино +Скопин +Славгород +Славск +Славянск-на-Кубани +Сланцы +Слободской +Слюдянка +Смоленск +Снежинск +Снежногорск +Собинка +Советск +Советская Гавань +Советский +Сокол +Солигалич +Соликамск +Солнечногорск +Соль-Илецк +Сольвычегодск +Сольцы +Сорочинск +Сорск +Сортавала +Сосенский +Сосновка +Сосновоборск +Сосновый Бор +Сосногорск +Сочи +Спас-Деменск +Спас-Клепики +Спасск +Спасск-Дальний +Спасск-Рязанский +Среднеколымск +Среднеуральск +Сретенск +Ставрополь +Старая Купавна +Старая Русса +Старица +Стародуб +Старый Крым +Старый Оскол +Стерлитамак +Стрежевой +Строитель +Струнино +Ступино +Суворов +Судак +Суджа +Судогда +Суздаль +Сунжа +Суоярви +Сураж +Сургут +Суровикино +Сурск +Сусуман +Сухиничи +Сухой Лог +Сызрань +Сыктывкар +Сысерть +Сычёвка +Сясьстрой +Тавда +Таганрог +Тайга +Тайшет +Талдом +Талица +Тамбов +Тара +Тарко-Сале +Таруса +Татарск +Таштагол +Тверь +Теберда +Тейково +Темников +Темрюк +Терек +Тетюши +Тимашёвск +Тихвин +Тихорецк +Тобольск +Тогучин +Тольятти +Томари +Томмот +Томск +Топки +Торжок +Торопец +Тосно +Тотьма +Троицк +Трубчевск +Трёхгорный +Туапсе +Туймазы +Тула +Тулун +Туран +Туринск +Тутаев +Тында +Тырныауз +Тюкалинск +Тюмень +Уварово +Углегорск +Углич +Удачный +Удомля +Ужур +Узловая +Улан-Удэ +Ульяновск +Унеча +Урай +Урень +Уржум +Урус-Мартан +Урюпинск +Усинск +Усмань +Усолье +Усолье-Сибирское +Уссурийск +Усть-Джегута +Усть-Илимск +Усть-Катав +Усть-Кут +Усть-Лабинск +Устюжна +Уфа +Ухта +Учалы +Уяр +Фатеж +Феодосия +Фокино +Фролово +Фрязино +Фурманов +Хабаровск +Хадыженск +Ханты-Мансийск +Харабали +Харовск +Хасавюрт +Хвалынск +Хилок +Химки +Холм +Холмск +Хотьково +Цивильск +Цимлянск +Циолковский +Чадан +Чайковский +Чапаевск +Чаплыгин +Чебаркуль +Чебоксары +Чегем +Чекалин +Челябинск +Чердынь +Черемхово +Черепаново +Череповец +Черкесск +Черноголовка +Черногорск +Чернушка +Черняховск +Чехов +Чистополь +Чита +Чкаловск +Чудово +Чулым +Чусовой +Чухлома +Чёрмоз +Шагонар +Шадринск +Шали +Шарыпово +Шарья +Шатура +Шахты +Шахунья +Шацк +Шебекино +Шелехов +Шенкурск +Шилка +Шимановск +Шиханы +Шлиссельбург +Шумерля +Шумиха +Шуя +Щербинка +Щигры +Щучье +Щёкино +Щёлкино +Щёлково +Электрогорск +Электросталь +Электроугли +Элиста +Энгельс +Эртиль +Югорск +Южа +Южно-Сахалинск +Южно-Сухокумск +Южноуральск +Юрга +Юрьев-Польский +Юрьевец +Юрюзань +Юхнов +Ядрин +Якутск +Ялта +Ялуторовск +Янаул +Яранск +Яровое +Ярославль +Ярцево +Ясногорск +Ясный +Яхрома diff --git a/src/test/java/ru/pdguard/detect/ToponymDictionaryTest.java b/src/test/java/ru/pdguard/detect/ToponymDictionaryTest.java new file mode 100644 index 0000000..3828fef --- /dev/null +++ b/src/test/java/ru/pdguard/detect/ToponymDictionaryTest.java @@ -0,0 +1,42 @@ +package ru.pdguard.detect; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Словарь городов — проверка вместо любого слова с заглавной буквы после «г.». */ +class ToponymDictionaryTest { + + @Test + void recognisesNominativeCase() { + assertTrue(ToponymDictionary.isKnownCity("Москва")); + assertTrue(ToponymDictionary.isKnownCity("Казань")); + assertTrue(ToponymDictionary.isKnownCity("Санкт-Петербург")); + } + + @Test + void recognisesInflectedForms() { + assertTrue(ToponymDictionary.isKnownCity("Москве"), "дательный падеж города на гласную"); + assertTrue(ToponymDictionary.isKnownCity("Тамбове"), "предложный падеж города на согласную"); + assertTrue(ToponymDictionary.isKnownCity("Казани"), "родительный падеж"); + } + + @Test + void recognisesCisCapitals() { + assertTrue(ToponymDictionary.isKnownCity("Минск")); + assertTrue(ToponymDictionary.isKnownCity("Алматы")); + } + + @Test + void rejectsMadeUpWord() { + assertFalse(ToponymDictionary.isKnownCity("Ерунда")); + assertFalse(ToponymDictionary.isKnownCity("Бла-бла")); + } + + @Test + void isCaseInsensitive() { + assertTrue(ToponymDictionary.isKnownCity("МОСКВА")); + assertTrue(ToponymDictionary.isKnownCity("москва")); + } +}