package ru.pdguard.detect; import static ru.pdguard.detect.RulePatterns.CITIZENSHIP_GAP; import static ru.pdguard.detect.RulePatterns.CITIZENSHIP_VALUE; import static ru.pdguard.detect.RulePatterns.ORGANISATION_NEARBY; import static ru.pdguard.detect.RulePatterns.ROLE_GAP; import static ru.pdguard.detect.RulePatterns.STREET_NAME; import static ru.pdguard.detect.RuleRegistry.ADDRESS_NEARBY; import java.util.List; /** Правила распознавания органа выдачи паспорта, места рождения, гражданства и адреса. */ final class AddressRules { private AddressRules() {} static final List RULES = List.of( // «выдан ОУФМС России по г. Москве 12.05.2015» — дата в состав органа не входит, // её забирает отдельное правило. Приоритет выше городского, иначе от органа // осталась бы замаскированной только его часть. // Перечень форм, не голая основа «выда»: она зацепила бы и «выдающийся» // (обычное слово, не про выдачу документа). Rule.of( PdTypes.PASSPORT_ISSUER, "(?iu:выдан|выдал[аио]?|выдали|выдач[аи]|выдаче)" + "\\W{0,3}([^,;\\n]{3,90}?)" + "(?=\\s*\\d{1,2}[.\\-/]\\d{1,2}[.\\-/]\\d{2,4}|[,;\\n]|\\s*$)", 78) .groups(1) .anchoredBy("выдан", "выдал", "выдач"), // «совпадает с указанным в анкете: X» — второе упоминание органа выдачи // под собственным якорем, без бэкреференса на первое. Rule.of( PdTypes.PASSPORT_ISSUER, "(?iu:указанн\\w*\\s+в\\s+анкете)\\W{0,5}([^,;.\\n]{3,90}?)" + "(?=[,;.\\n]|\\s*$)", 78) .groups(1) .anchoredBy("указанн"), // «Орган выдачи УФМС России по Республике Татарстан» — орган после якоря, // до слова «совпадает» или конца фразы. Rule.of( PdTypes.PASSPORT_ISSUER, "(?iu:орган\\s+выдачи)\\W{0,5}([^,;:\\n]{3,90}?)" + "(?=\\s*(?iu:совпадает|указанн)|[,;:\\n]|\\s*$)", 78) .groups(1) .anchoredBy("орган выдачи"), Rule.of( PdTypes.BIRTH_PLACE, "(?iu:мест\\w*\\s+рождения)\\W{0,5}([^,;\\n]{3,60}?)(?=\\s*[,;\\n]|\\s*$)", 76) .groups(1) .anchoredBy("рождения"), Rule.of( PdTypes.BIRTH_PLACE, "(?iu:родил(?:ся|ась))[^,;\\n]{0,40}?\\s+в\\s+" + "([^,;\\n]{3,40}?)(?=\\s*[,;\\n]|\\s*$)", 76) .groups(1) .anchoredBy("родил"), // ROLE_GAP, не \W{0,5}: «Гражданство бенефициара по договору страхования: Х» — // между якорем и значением бывает несколько слов, не только пунктуация. // Список через запятую/слэш — вторая опциональная группа тем же шаблоном. Rule.of( PdTypes.CITIZENSHIP, "(?iu:гражданств)\\w*" + CITIZENSHIP_GAP + "(" + CITIZENSHIP_VALUE + ")(?:\\s*[,/]\\s*(" + CITIZENSHIP_VALUE + "))?", 80) .groups(1, 2) .validatedBy(CountryDictionary::isKnownCountry) .anchoredBy("гражданств"), // ин/ка/ина/ки — именительный/родительный; ином/кой — творительный // («гражданином», «гражданкой»). Rule.of( PdTypes.CITIZENSHIP, "(?iu:граждан(?:ин|ка|ина|ки|ином|кой))\\b\\s+" + "(" + CITIZENSHIP_VALUE + ")(?:\\s*[,/]\\s*(" + CITIZENSHIP_VALUE + "))?", 75) .groups(1, 2) .validatedBy(CountryDictionary::isKnownCountry) .anchoredBy("граждан"), // --- Адрес: каждая составляющая настраивается отдельно --- Rule.of(PdTypes.ADDRESS_POSTCODE, "(?iu:индекс)\\w*" + ROLE_GAP + "(\\d{6})\\b", 74) .groups(1) .vetoedBy(ORGANISATION_NEARBY) .anchoredBy("индекс"), Rule.of( PdTypes.ADDRESS_POSTCODE, "\\b(\\d{6})(?=\\s*,?\\s*(?iu:г\\.|город|обл\\.|область|респ|край))", 74) .groups(1) .vetoedBy(ORGANISATION_NEARBY), // Не только «г.»: перепись, на которой проверяется словарь, покрывает // сёла, посёлки, деревни, хутора и станицы — «рп. Ильинское», «с. Кукуево» // из ТЗ без этих якорей не нашлись бы вообще, город там ни при чём. Rule.of( PdTypes.ADDRESS_CITY, "(?iu:\\bг\\.|\\bгор\\.|\\bгород|\\bрп\\.|\\bпгт\\.?|\\bп\\.|\\bс\\.|\\bсело\\b" + "|\\bд\\.|\\bдеревня\\b|\\bдер\\.|\\bх\\.|\\bхутор\\b|\\bст-ца|\\bстаница|\\bаул\\b" + "|\\bсл\\.|\\bслобода\\b|\\bаал\\b)\\s?(\\p{Lu}[\\p{L}-]{1,30})\\b", 73) .groups(1) .validatedBy(ToponymDictionary::isKnownSettlement) .vetoedBy(ORGANISATION_NEARBY) .anchoredBy( "г.", "гор", "город", "рп.", "пгт", "п.", "с.", "село", "д.", "деревня", "дер.", "х.", "хутор", "ст-ца", "станица", "аул", "сл.", "слобода", "аал"), Rule.of( PdTypes.ADDRESS_STREET, "(?iu:\\bул\\.|\\bулиц\\p{L}*|\\bпр-т|\\bпроспект\\p{L}*|\\bпер\\.|\\bпереул\\p{L}*" + "|\\bш\\.|\\bшоссе|\\bб-р|\\bбульвар\\p{L}*|\\bнаб\\.|\\bнабережн\\p{L}*)" + "\\W{0,3}(" + STREET_NAME + ")", 73) .groups(1) .vetoedBy(ORGANISATION_NEARBY) .requiringNear(ADDRESS_NEARBY) .anchoredBy("ул", "просп", "пр-т", "пер.", "шоссе", "ш.", "бульвар", "б-р", "наб"), // «Невский пр-т» — указатель после названия. Форма слишком общая, поэтому // принимается только рядом с другими частями адреса: иначе под маску попал бы // любой рассказ про Невский проспект. Rule.of( PdTypes.ADDRESS_STREET, "\\b(\\p{Lu}[\\p{L}-]{2,30})\\s+" + "(?iu:пр-т|проспект|улиц\\p{L}*|шоссе|бульвар|переул\\p{L}*|набережн\\p{L}*)\\b", 73) .groups(1) .vetoedBy(ORGANISATION_NEARBY) .requiringNear(ADDRESS_NEARBY) .anchoredBy("пр-т", "проспект", "улиц", "шоссе", "бульвар", "переул", "набережн"), Rule.of( PdTypes.ADDRESS_HOUSE, "(?iu:\\bд\\.|\\bдом)\\s?(\\d+\\p{L}?(?:\\s?(?iu:к\\.|корп\\.?|стр\\.)\\s?\\d+)?)\\b", 72) .groups(1) .vetoedBy(ORGANISATION_NEARBY) .anchoredBy("д.", "дом"), Rule.of(PdTypes.ADDRESS_FLAT, "(?iu:\\bкв\\.|\\bквартир\\p{L}*)\\s?(\\d+\\p{L}?)\\b", 72) .groups(1) .vetoedBy(ORGANISATION_NEARBY) .anchoredBy("кв"), Rule.of( PdTypes.ADDRESS_COUNTRY, "(?iu:стран\\p{L}*(?:\\s+(?:регистрации|проживания|гражданства))?)" + "\\W{0,5}(\\p{Lu}[\\p{L}-]{2,30})\\b", 71) .groups(1) .vetoedBy(ORGANISATION_NEARBY) .anchoredBy("стран")); }