from yargy import (
    rule,
    or_, and_
)
from yargy.interpretation import fact
from yargy.predicates import (
    eq, lte, gte, gram, type, tag,
    length_eq,
    in_, in_caseless, dictionary,
    normalized, caseless,
    is_title
)
from yargy.pipelines import morph_pipeline
from yargy.tokenizer import QUOTES

Index = fact(
    'Index',
    ['value']
)
Country = fact(
    'Country',
    ['name']
)
Region = fact(
    'Region',
    ['name', 'type']
)
Settlement = fact(
    'Settlement',
    ['name', 'type']
)
Street = fact(
    'Street',
    ['name', 'type']
)
Building = fact(
    'Building',
    ['number', 'type']
)
Room = fact(
    'Room',
    ['number', 'type']
)
AddrPart = fact(
    'AddrPart',
    ['value']
)


def value(key):
    @property
    def field(self):
        return getattr(self, key)
    return field


class Index(Index):
    type = 'индекс'


class Country(Country):
    type = 'страна'
    value = value('name')


class Region(Region):
    value = value('name')


class Settlement(Settlement):
    value = value('name')


class Street(Settlement):
    value = value('name')


class Building(Building):
    value = value('number')


class Room(Room):
    value = value('number')


class AddrPart(AddrPart):
    @property
    def obj(self):
        from natasha import obj

        part = self.value
        return obj.AddrPart(part.value, part.type)


DASH = eq('-')
DOT = eq('.')

ADJF = gram('ADJF')
NOUN = gram('NOUN')
INT = type('INT')
TITLE = is_title()

ANUM = rule(
    INT,
    DASH.optional(),
    in_caseless({
        'я', 'й', 'е',
        'ое', 'ая', 'ий', 'ой'
    })
)

from yargy.predicates import custom

def is_address_token(word):
  match = re.compile('[-a-zA-Zа-яёА-ЯЁ.0-9"«»№\/]+').match(word)
  if match is None:
    return False
  return match.span()[1] == len(word)

class Something(Settlement):
    value = value('text')

SOMETHING = rule( #то ли район, то ли город... - набор слов без знаков препинания, возможно с запятой в конце
    custom(is_address_token).repeatable()
).interpretation(
    Something
)

# def is_address_token_extended(word):
#   match = re.compile('[-a-zA-Zа-яёА-ЯЁ.0-9"«»№]+').match(word)
#   if match is None:
#     return False
#   return match.span()[1] == len(word)

# SOMETHING_EXTENDED = rule( #то ли район, то ли город... - набор слов без знаков препинания, возможно с запятой в конце
#     custom(is_address_token_extended).repeatable()
# )


#########
#
#  STRANA
#
##########


# TODO
# COUNTRY_VALUE = dictionary({
#     'россия',
#     'украина'
# })

# ABBR_COUNTRY_VALUE = in_caseless({
#     'рф'
# })

COUNTRY = morph_pipeline([
    'россия',
    'украина',
    'рф',
    'российская федерация'
]).interpretation(
    Country.name
).interpretation(
    Country
)

# COUNTRY = or_(
#     COUNTRY_VALUE,
#     ABBR_COUNTRY_VALUE,
#     rule(caseless('российская'), caseless('федерация'))
# ).interpretation(
#     Country.name
# ).interpretation(
#     Country
# )


#############
#
#  FED OKRUGA
#
############


FED_OKRUG_NAME = or_(
    rule(
        dictionary({
            'дальневосточный',
            'приволжский',
            'сибирский',
            'уральский',
            'центральный',
            'южный',
        })
    ),
    rule(
        caseless('северо'),
        DASH.optional(),
        dictionary({
            'западный',
            'кавказский'
        })
    )
).interpretation(
    Region.name
)

FED_OKRUG_WORDS = or_(
    rule(
        normalized('федеральный'),
        normalized('округ')
    ),
    rule(caseless('фо'))
).interpretation(
    Region.type.const('федеральный округ')
)

FED_OKRUG = rule(
    FED_OKRUG_WORDS,
    FED_OKRUG_NAME
).interpretation(
    Region
)


#########
#
#   RESPUBLIKA
#
############


RESPUBLIKA_WORDS = or_(
    rule(caseless('респ'), DOT.optional()),
    rule(normalized('республика'))
).interpretation(
    Region.type.const('республика')
)

RESPUBLIKA_ADJF = or_(
    rule(
        dictionary({
            'удмуртский',
            'чеченский',
            'чувашский',
            'карачаево-черкесский',
            'кабардино-балкарский'
        })
    )
).interpretation(
    Region.name
)

RESPUBLIKA_NAME = or_(
    rule(
        dictionary({
            'адыгея',
            'алтай',
            'башкортостан',
            'бурятия',
            'дагестан',
            'ингушетия',
            'калмыкия',
            'карелия',
            'коми',
            'крым',
            'мордовия',
            'татарстан',
            'тыва',
            'удмуртия',
            'хакасия',
            'саха',
            'якутия',
        })
    ),
    rule(caseless('марий'), caseless('эл')),
    rule(caseless('саха'), eq('('), caseless('якутия'), eq(')')),
    rule(
        normalized('северный'),
        dictionary({'осетия-алания', 'осетия'}).optional()
    )
).interpretation(
    Region.name
)

RESPUBLIKA_ABBR = in_caseless({
    'кбр',
    'кчр',
    'рт',  # Татарстан
}).interpretation(
    Region.name  # TODO type
)

RESPUBLIKA = or_(
    rule(RESPUBLIKA_ADJF, RESPUBLIKA_WORDS),
    rule(RESPUBLIKA_WORDS, RESPUBLIKA_NAME),
    rule(RESPUBLIKA_ABBR)
).interpretation(
    Region
)


##########
#
#   KRAI
#
########


KRAI_WORDS = normalized('край').interpretation(
    Region.type.const('край')
)

KRAI_NAME = dictionary({
    'алтайский',
    'забайкальский',
    'камчатский',
    'краснодарский',
    'красноярский',
    'пермский',
    'приморский',
    'ставропольский',
    'хабаровский',
}).interpretation(
    Region.name
)

KRAI = rule(
    KRAI_NAME, KRAI_WORDS
).interpretation(
    Region
)


############
#
#    OBLAST
#
############


OBLAST_WORDS = or_(
    rule(normalized('область')),
    rule(
        caseless('обл'),
        DOT.optional()
    )
).interpretation(
    Region.type.const('область')
)

OBLAST_NAME = dictionary({
    'амурский',
    'архангельский',
    'астраханский',
    'белгородский',
    'брянский',
    'владимирский',
    'волгоградский',
    'вологодский',
    'воронежский',
    'горьковский',
    'ивановский',
    'ивановский',
    'иркутский',
    'калининградский',
    'калужский',
    'камчатский',
    'кемеровский',
    'кировский',
    'костромской',
    'курганский',
    'курский',
    'ленинградский',
    'липецкий',
    'магаданский',
    'московский',
    'мурманский',
    'нижегородский',
    'новгородский',
    'новосибирский',
    'омский',
    'оренбургский',
    'орловский',
    'пензенский',
    'пермский',
    'псковский',
    'ростовский',
    'рязанский',
    'самарский',
    'саратовский',
    'сахалинский',
    'свердловский',
    'смоленский',
    'тамбовский',
    'тверской',
    'томский',
    'тульский',
    'тюменский',
    'ульяновский',
    'челябинский',
    'читинский',
    'ярославский',
}).interpretation(
    Region.name
)

OBLAST = rule(
    OBLAST_NAME,
    OBLAST_WORDS
).interpretation(
    Region
)


##########
#
#    AUTO OKRUG
#
#############


AUTO_OKRUG_NAME = or_(
    rule(
        dictionary({
            'чукотский',
            'эвенкийский',
            'корякский',
            'ненецкий',
            'таймырский',
            'агинский',
            'бурятский',
            'коми-пермяцкий',
            'долгано-ненецкий',
            'ямало-ненецкий'
        })
    ),
    # rule(caseless('коми'), '-', normalized('пермяцкий')),
    # rule(caseless('долгано'), '-', normalized('ненецкий')),
    # rule(caseless('ямало'), '-', normalized('ненецкий')),
).interpretation(
    Region.name
)

AUTO_OKRUG_WORDS = or_(
    rule(
        normalized('автономный'),
        normalized('округ')
    ),
    rule(caseless('ао'))
).interpretation(
    Region.type.const('автономный округ')
)

HANTI = rule(
    #caseless('ханты'), '-', normalized('мансийский')
    normalized('ханты-мансийский')
).interpretation(
    Region.name
)

BURAT = rule(
    #caseless('усть'), '-', normalized('ордынский'),
    normalized('усть-ордынский'),
    normalized('бурятский')
).interpretation(
    Region.name
)

AUTO_OKRUG = or_(
    rule(AUTO_OKRUG_NAME, AUTO_OKRUG_WORDS),
    or_(
        rule(
            HANTI, AUTO_OKRUG_WORDS, '-', normalized('югра')
        ),
        rule(HANTI, AUTO_OKRUG_WORDS),
        rule(
            caseless('хмао'),
        ).interpretation(Region.name),
        rule(
            caseless('хмао'), '-', caseless('югра')
        ).interpretation(Region.name),
    ),
    rule(
        BURAT,
        AUTO_OKRUG_WORDS
    )
).interpretation(
    Region
)


##########
#
#  RAION
#
###########


RAION_WORDS = or_(
    #rule(caseless('р'), '-', in_caseless({'он', 'н'})),
    rule(normalized('р-н')),
    rule(normalized('р-он')),
    rule(normalized('рн')),
    rule(normalized('район')),
    rule(normalized('микрорайон')),
    rule(normalized('м-н')),
    rule(normalized('мкр-н'))
).interpretation(
    Region.type.const('район')
)

RAION_SIMPLE_NAME = and_(
    ADJF,
    TITLE
)

RAION_MODIFIERS = rule(
    in_caseless({
        'усть',
        'северо',
        'александрово',
        'гаврилово',
    }),
    DASH.optional(),
    TITLE
)

RAION_COMPLEX_NAME = rule(
    RAION_MODIFIERS,
    RAION_SIMPLE_NAME
)

RAION_NAME = or_(
    rule(RAION_SIMPLE_NAME),
    RAION_COMPLEX_NAME
).interpretation(
    Region.name
)



# RAION = rule(
#     RAION_NAME,
#     RAION_WORDS
# ).interpretation(
#     Region
# )

RAION = or_(
  rule(
    #RAION_NAME,
    SOMETHING,
    RAION_WORDS
  ),
  rule(
    RAION_WORDS,
    #RAION_NAME
    SOMETHING
  )
).interpretation(
  Region
)


###########
#
#   GOROD
#
###########


# Top 200 Russia cities, cover 75% of population

# COMPLEX = morph_pipeline([
#     'санкт-петербург',
#     'нижний новгород',
#     'н.новгород',
#     'ростов-на-дону',
#     'набережные челны',
#     'улан-удэ',
#     'нижний тагил',
#     'комсомольск-на-амуре',
#     'йошкар-ола',
#     'старый оскол',
#     'великий новгород',
#     'южно-сахалинск',
#     'петропавловск-камчатский',
#     'каменск-уральский',
#     'орехово-зуево',
#     'сергиев посад',
#     'новый уренгой',
#     'ленинск-кузнецкий',
#     'великие луки',
#     'каменск-шахтинский',
#     'усть-илимск',
#     'усолье-сибирский',
#     'кирово-чепецк',
# ])

CITIES_TOWNS = morph_pipeline({'Абаза', 'Абакан', 'Абдулино', 'Абинск', 'Агидель', 'Агрыз', 'Адыгейск', 'Азнакаево', 'Азов', 
    'Ак-Довурак', 'Аксай', 'Алагир', 'Алапаевск', 'Алатырь', 'Алдан', 'Алейск', 'Александров', 'Александровск', 'Александровск-Сахалинский', 
    'Алексеевка', 'Алексин', 'Алзамай', 'Алупка', 'Алушта', 'Альметьевск', 'Амурск', 'Анадырь', 'Анапа', 'Ангарск', 
    'Андреаполь', 'Анжеро-Судженск', 'Анива', 'Апатиты', 'Апрелевка', 'Апшеронск', 'Арамиль', 'Аргун', 'Ардатов', 'Ардон', 
    'Арзамас', 'Аркадак', 'Армавир', 'Армянск', 'Армянськ', 'Арсеньев', 'Арск', 'Артем', 'Артемовск', 'Артемовский', 
    'Архангельск', 'Асбест', 'Асино', 'Астрахань', 'Аткарск', 'Ахтубинск', 'Ахтубинск-7', 'Ачинск', 'Аша', 'Бабаево', 
    'Бабушкин', 'Бавлы', 'Багратионовск', 'Байкальск', 'Баймак', 'Бакал', 'Баксан', 'Балабаново', 'Балаково', 'Балахна', 
    'Балашиха', 'Балашов', 'Балей', 'Балтийск', 'Барабинск', 'Барнаул', 'Барыш', 'Батайск', 'Бахчисарай', 'Бежецк', 
    'Белая Калитва', 'Белая Холуница', 'Белгород', 'Белебей', 'Белев', 'Белинский', 'Белово', 'Белогорск', 'Белогорск', 'Белозерск', 
    'Белокуриха', 'Беломорск', 'Белорецк', 'Белореченск', 'Белоусово', 'Белоярский', 'Белый', 'Бердск', 'Березники', 'Березовский', 
    'Березовский', 'Беслан', 'Бийск', 'Бикин', 'Билибино', 'Биробиджан', 'Бирск', 'Бирюсинск', 'Бирюч', 'Благовещенск', 
    'Благовещенск', 'Благодарный', 'Бобров', 'Богданович', 'Богородицк', 'Богородск', 'Боготол', 'Богучар', 'Бодайбо', 'Бокситогорск', 
    'Болгар', 'Бологое', 'Болотное', 'Болохово', 'Болхов', 'Большой Камень', 'Бор', 'Борзя', 'Борисоглебск', 'Боровичи', 
    'Боровск', 'Боровск-1', 'Бородино', 'Братск', 'Бронницы', 'Брянск', 'Бугульма', 'Бугуруслан', 'Буденновск', 'Бузулук', 
    'Буинск', 'Буй', 'Буйнакск', 'Бутурлиновка', 'Валдай', 'Валуйки', 'Велиж', 'Великие Луки', 'Великие Луки-1', 'Великий Новгород', 
    'Великий Устюг', 'Вельск', 'Венев', 'Верещагино', 'Верея', 'Верхнеуральск', 'Верхний Тагил', 'Верхний Уфалей', 'Верхняя Пышма', 'Верхняя Салда', 
    'Верхняя Тура', 'Верхотурье', 'Верхоянск', 'Весьегонск', 'Ветлуга', 'Видное', 'Вилюйск', 'Вилючинск', 'Вихоревка', 'Вичуга', 
    'Владивосток', 'Владикавказ', 'Владимир', 'Волгоград', 'Волгодонск', 'Волгореченск', 'Волжск', 'Волжский', 'Вологда', 'Володарск', 
    'Волоколамск', 'Волосово', 'Волхов', 'Волчанск', 'Вольск', 'Вольск-18', 'Воркута', 'Воронеж', 'Воронеж-45', 'Ворсма', 
    'Воскресенск', 'Воткинск', 'Всеволожск', 'Вуктыл', 'Выборг', 'Выкса', 'Высоковск', 'Высоцк', 'Вытегра', 'Вышний Волочек', 
    'Вяземский', 'Вязники', 'Вязьма', 'Вятские Поляны', 'Гаврилов Посад', 'Гаврилов-Ям', 'Гагарин', 'Гаджиево', 'Гай', 'Галич', 
    'Гатчина', 'Гвардейск', 'Гдов', 'Геленджик', 'Георгиевск', 'Глазов', 'Голицыно', 'Горбатов', 'Горно-Алтайск', 'Горнозаводск', 
    'Горняк', 'Городец', 'Городище', 'Городовиковск', 'Городской округ Черноголовка', 'Гороховец', 'Горячий Ключ', 'Грайворон', 'Гремячинск', 'Грозный', 
    'Грязи', 'Грязовец', 'Губаха', 'Губкин', 'Губкинский', 'Гудермес', 'Гуково', 'Гулькевичи', 'Гурьевск', 'Гурьевск', 
    'Гусев', 'Гусиноозерск', 'Гусь-Хрустальный', 'Давлеканово', 'Дагестанские Огни', 'Далматово', 'Дальнегорск', 'Дальнереченск', 'Данилов', 'Данков', 
    'Дегтярск', 'Дедовск', 'Демидов', 'Дербент', 'Десногорск', 'Джанкой', 'Джанкой', 'Дзержинск', 'Дзержинский', 'Дивногорск', 
    'Дигора', 'Димитровград', 'Дмитриев', 'Дмитров', 'Дмитровск', 'Дно', 'Добрянка', 'Долгопрудный', 'Долинск', 'Домодедово', 
    'Донецк', 'Донской', 'Дорогобуж', 'Дрезна', 'Дубна', 'Дубовка', 'Дудинка', 'Духовщина', 'Дюртюли', 'Дятьково', 
    'Евпатория', 'Егорьевск', 'Ейск', 'Екатеринбург', 'Елабуга', 'Елец', 'Елизово', 'Ельня', 'Еманжелинск', 'Емва', 
    'Енисейск', 'Ермолино', 'Ершов', 'Ессентуки', 'Ефремов', 'Железноводск', 'Железногорск', 'Железногорск', 'Железногорск-Илимский', 'Железнодорожный', 
    'Жердевка', 'Жигулевск', 'Жиздра', 'Жирновск', 'Жуков', 'Жуковка', 'Жуковский', 'Завитинск', 'Заводоуковск', 'Заволжск', 
    'Заволжье', 'Задонск', 'Заинск', 'Закаменск', 'Заозерный', 'Заозерск', 'Западная Двина', 'Заполярный', 'Зарайск', 'Заречный', 
    'Заречный', 'Заринск', 'Звенигово', 'Звенигород', 'Зверево', 'Зеленогорск', 'Зеленогорск', 'Зеленоград', 'Зеленоградск', 'Зеленодольск', 
    'Зеленокумск', 'Зерноград', 'Зея', 'Зима', 'Златоуст', 'Злынка', 'Змеиногорск', 'Знаменск', 'Зубцов', 'Зуевка', 
    'Ивангород', 'Иваново', 'Ивантеевка', 'Ивдель', 'Игарка', 'Ижевск', 'Избербаш', 'Изобильный', 'Иланский', 'Инза', 
    'Инкерман', 'Инсар', 'Инта', 'Ипатово', 'Ирбит', 'Иркутск', 'Иркутск-45', 'Исилькуль', 'Искитим', 'Истра', 
    'Истра-1', 'Ишим', 'Ишимбай', 'Йошкар-Ола', 'Кадников', 'Казань', 'Калач', 'Калачинск', 'Калач-на-Дону', 'Калининград', 
    'Калининск', 'Калтан', 'Калуга', 'Калязин', 'Камбарка', 'Каменка', 'Каменногорск', 'Каменск-Уральский', 'Каменск-Шахтинский', 'Камень-на-Оби', 
    'Камешково', 'Камызяк', 'Камышин', 'Камышлов', 'Канаш', 'Кандалакша', 'Канск', 'Карабаново', 'Карабаш', 'Карабулак', 
    'Карасук', 'Карачаевск', 'Карачев', 'Каргат', 'Каргополь', 'Карпинск', 'Карталы', 'Касимов', 'Касли', 'Каспийск', 
    'Катав-Ивановск', 'Катайск', 'Качканар', 'Кашин', 'Кашира', 'Кашира-8', 'Кедровый', 'Кемерово', 'Кемь', 'Керчь', 
    'Кизел', 'Кизилюрт', 'Кизляр', 'Кимовск', 'Кимры', 'Кингисепп', 'Кинель', 'Кинешма', 'Киреевск', 'Киренск', 
    'Киржач', 'Кириллов', 'Кириши', 'Киров', 'Киров', 'Кировград', 'Кирово-Чепецк', 'Кировск', 'Кировск', 'Кирс', 
    'Кирсанов', 'Киселевск', 'Кисловодск', 'Климовск', 'Клин', 'Клинцы', 'Княгинино', 'Ковдор', 'Ковров', 'Ковылкино', 
    'Когалым', 'Кодинск', 'Козельск', 'Козловка', 'Козьмодемьянск', 'Кола', 'Кологрив', 'Коломна', 'Колпашево', 'Колпино', 
    'Кольчугино', 'Коммунар', 'Комсомольск', 'Комсомольск-на-Амуре', 'Конаково', 'Кондопога', 'Кондрово', 'Константиновск', 'Копейск', 'Кораблино', 
    'Кореновск', 'Коркино', 'Королев', 'Короча', 'Корсаков', 'Коряжма', 'Костерево', 'Костомукша', 'Кострома', 'Котельники', 
    'Котельниково', 'Котельнич', 'Котлас', 'Котово', 'Котовск', 'Кохма', 'Красавино', 'Красноармейск', 'Красноармейск', 'Красновишерск', 
    'Красногорск', 'Краснодар', 'Красное Село', 'Краснозаводск', 'Краснознаменск', 'Краснознаменск', 'Краснокаменск', 'Краснокамск', 'Красноперекопск', 'Красноперекопск', 
    'Краснослободск', 'Краснослободск', 'Краснотурьинск', 'Красноуральск', 'Красноуфимск', 'Красноярск', 'Красный Кут', 'Красный Сулин', 'Красный Холм', 'Кременки', 
    'Кронштадт', 'Кропоткин', 'Крымск', 'Кстово', 'Кубинка', 'Кувандык', 'Кувшиново', 'Кудымкар', 'Кузнецк', 'Кузнецк-12', 
    'Кузнецк-8', 'Куйбышев', 'Кулебаки', 'Кумертау', 'Кунгур', 'Купино', 'Курган', 'Курганинск', 'Курильск', 'Курлово', 
    'Куровское', 'Курск', 'Куртамыш', 'Курчатов', 'Куса', 'Кушва', 'Кызыл', 'Кыштым', 'Кяхта', 'Лабинск', 
    'Лабытнанги', 'Лагань', 'Ладушкин', 'Лаишево', 'Лакинск', 'Лангепас', 'Лахденпохья', 'Лебедянь', 'Лениногорск', 'Ленинск', 
    'Ленинск-Кузнецкий', 'Ленск', 'Лермонтов', 'Лесной', 'Лесозаводск', 'Лесосибирск', 'Ливны', 'Ликино-Дулево', 'Липецк', 'Липки', 
    'Лиски', 'Лихославль', 'Лобня', 'Лодейное Поле', 'Ломоносов', 'Лосино-Петровский', 'Луга', 'Луза', 'Лукоянов', 'Луховицы', 
    'Лысково', 'Лысьва', 'Лыткарино', 'Льгов', 'Любань', 'Люберцы', 'Любим', 'Людиново', 'Лянтор', 'Магадан', 
    'Магас', 'Магнитогорск', 'Майкоп', 'Майский', 'Макаров', 'Макарьев', 'Макушино', 'Малая Вишера', 'Малгобек', 'Малмыж', 
    'Малоархангельск', 'Малоярославец', 'Мамадыш', 'Мамоново', 'Мантурово', 'Мариинск', 'Мариинский Посад', 'Маркс', 'Махачкала', 'Мглин', 
    'Мегион', 'Медвежьегорск', 'Медногорск', 'Медынь', 'Межгорье', 'Междуреченск', 'Мезень', 'Меленки', 'Мелеуз', 'Менделеевск', 
    'Мензелинск', 'Мещовск', 'Миасс', 'Микунь', 'Миллерово', 'Минеральные Воды', 'Минусинск', 'Миньяр', 'Мирный', 'Мирный', 
    'Михайлов', 'Михайловка', 'Михайловск', 'Михайловск', 'Мичуринск', 'Могоча', 'Можайск', 'Можга', 'Моздок', 'Мончегорск', 
    'Морозовск', 'Моршанск', 'Мосальск', 'Москва', 'Московский', 'Московский', 'Муравленко', 'Мураши', 'Мурманск', 'Муром', 
    'Мценск', 'Мыски', 'Мытищи', 'Мышкин', 'Набережные Челны', 'Навашино', 'Наволоки', 'Надым', 'Назарово', 'Назрань', 
    'Называевск', 'Нальчик', 'Нариманов', 'Наро-Фоминск', 'Нарткала', 'Нарьян-Мар', 'Находка', 'Невель', 'Невельск', 'Невинномысск', 
    'Невьянск', 'Нелидово', 'Неман', 'Нерехта', 'Нерчинск', 'Нерюнгри', 'Нестеров', 'Нефтегорск', 'Нефтекамск', 'Нефтекумск', 
    'Нефтеюганск', 'Нея', 'Нижневартовск', 'Нижнекамск', 'Нижнеудинск', 'Нижние Серги', 'Нижние Серги-3', 'Нижний Ломов', 'Нижний Новгород', 'Нижний Тагил', 
    'Нижняя Салда', 'Нижняя Тура', 'Николаевск', 'Николаевск-на-Амуре', 'Никольск', 'Никольск', 'Никольское', 'Новая Ладога', 'Новая Ляля', 'Новоалександровск', 
    'Новоалтайск', 'Новоаннинский', 'Нововоронеж', 'Новодвинск', 'Новозыбков', 'Новокубанск', 'Новокузнецк', 'Новокуйбышевск', 'Новомичуринск', 'Новомосковск', 
    'Новопавловск', 'Новоржев', 'Новороссийск', 'Новосибирск', 'Новосиль', 'Новосокольники', 'Новотроицк', 'Новоузенск', 'Новоульяновск', 'Новоуральск', 
    'Новохоперск', 'Новочебоксарск', 'Новочеркасск', 'Новошахтинск', 'Новый Оскол', 'Новый Уренгой', 'Ногинск', 'Нолинск', 'Норильск', 'Ноябрьск', 
    'Нурлат', 'Нытва', 'Нюрба', 'Нягань', 'Нязепетровск', 'Няндома', 'Облучье', 'Обнинск', 'Обоянь', 'Обь', 
    'Одинцово', 'Ожерелье', 'Озерск', 'Озерск', 'Озеры', 'Октябрьск', 'Октябрьский', 'Окуловка', 'Олекминск', 'Оленегорск', 
    'Оленегорск-1', 'Оленегорск-2', 'Оленегорск-4', 'Олонец', 'Омск', 'Омутнинск', 'Онега', 'Опочка', 'Орёл', 'Оренбург', 
    'Орехово-Зуево', 'Орлов', 'Орск', 'Оса', 'Осинники', 'Осташков', 'Остров', 'Островной', 'Острогожск', 'Отрадное', 
    'Отрадный', 'Оха', 'Оханск', 'Очер', 'Павлово', 'Павловск', 'Павловск', 'Павловский Посад', 'Палласовка', 'Партизанск', 
    'Певек', 'Пенза', 'Первомайск', 'Первоуральск', 'Перевоз', 'Пересвет', 'Переславль-Залесский', 'Пермь', 'Пестово', 'Петергоф', 
    'Петров Вал', 'Петровск', 'Петровск-Забайкальский', 'Петрозаводск', 'Петропавловск-Камчатский', 'Петухово', 'Петушки', 'Печора', 'Печоры', 'Пикалево', 
    'Пионерский', 'Питкяранта', 'Плавск', 'Пласт', 'Плес', 'Поворино', 'Подгорное', 'Подольск', 'Подпорожье', 'Покачи', 
    'Покров', 'Покровск', 'Полевской', 'Полесск', 'Полысаево', 'Полярные Зори', 'Полярный', 'Поронайск', 'Порхов', 'Похвистнево', 
    'Почеп', 'Починок', 'Пошехонье', 'Правдинск', 'Приволжск', 'Приморск', 'Приморск', 'Приморско-Ахтарск', 'Приозерск', 'Прокопьевск', 
    'Пролетарск', 'Протвино', 'Прохладный', 'Псков', 'Пугачев', 'Пудож', 'Пустошка', 'Пучеж', 'Пушкин', 'Пушкино', 
    'Пущино', 'Пыталово', 'Пыть-Ях', 'Пятигорск', 'Радужный', 'Радужный', 'Райчихинск', 'Раменское', 'Рассказово', 'Ревда', 
    'Реж', 'Реутов', 'Ржев', 'Родники', 'Рославль', 'Россошь', 'Ростов', 'Ростов-на-Дону', 'Рошаль', 'Ртищево', 
    'Рубцовск', 'Рудня', 'Руза', 'Рузаевка', 'Рыбинск', 'Рыбное', 'Рыльск', 'Ряжск', 'Рязань', 'Саки', 
    'Саки', 'Салават', 'Салаир', 'Салехард', 'Сальск', 'Самара', 'Санкт-Петербург', 'Саранск', 'Сарапул', 'Саратов', 
    'Саров', 'Сасово', 'Сатка', 'Сафоново', 'Саяногорск', 'Саянск', 'Светлогорск', 'Светлоград', 'Светлый', 'Светогорск', 
    'Свирск', 'Свободный', 'Себеж', 'Севастополь', 'Северобайкальск', 'Северодвинск', 'Северо-Курильск', 'Североморск', 'Североуральск', 'Северск', 
    'Севск', 'Сегежа', 'Сельцо', 'Семенов', 'Семикаракорск', 'Семилуки', 'Сенгилей', 'Серафимович', 'Сергач', 'Сергиев Посад', 
    'Сергиев Посад-7', 'Сердобск', 'Серов', 'Серпухов', 'Сертолово', 'Сестрорецк', 'Сибай', 'Сим', 'Симферополь', 'Сковородино', 
    'Скопин', 'Славгород', 'Славск', 'Славянск-на-Кубани', 'Сланцы', 'Слободской', 'Слюдянка', 'Смоленск', 'Снегири', 'Снежинск', 
    'Снежногорск', 'Собинка', 'Советск', 'Советск', 'Советск', 'Советская Гавань', 'Советский', 'Сокол', 'Солигалич', 'Соликамск', 
    'Солнечногорск', 'Солнечногорск-2', 'Солнечногорск-25', 'Солнечногорск-30', 'Солнечногорск-7', 'Сольвычегодск', 'Соль-Илецк', 'Сольцы', 'Сольцы 2', 'Сорочинск', 
    'Сорск', 'Сортавала', 'Сосенский', 'Сосновка', 'Сосновоборск', 'Сосновый Бор', 'Сосногорск', 'Сочи', 'Спас-Деменск', 'Спас-Клепики', 
    'Спасск', 'Спасск-Дальний', 'Спасск-Рязанский', 'Среднеколымск', 'Среднеуральск', 'Сретенск', 'Ставрополь', 'Старая Купавна', 'Старая Русса', 'Старица', 
    'Стародуб', 'Старый крым', 'Старый Оскол', 'Стерлитамак', 'Стрежевой', 'Строитель', 'Струнино', 'Ступино', 'Суворов', 'Судак', 
    'Суджа', 'Судогда', 'Суздаль', 'Суоярви', 'Сураж', 'Сургут', 'Суровикино', 'Сурск', 'Сусуман', 'Сухиничи', 
    'Сухой Лог', 'Сызрань', 'Сыктывкар', 'Сысерть', 'Сычевка', 'Сясьстрой', 'Тавда', 'Таганрог', 'Тайга', 'Тайшет', 
    'Талдом', 'Талица', 'Тамбов', 'Тара', 'Тарко-Сале', 'Таруса', 'Татарск', 'Таштагол', 'Тверь', 'Теберда', 
    'Тейково', 'Темников', 'Темрюк', 'Терек', 'Тетюши', 'Тимашевск', 'Тихвин', 'Тихорецк', 'Тобольск', 'Тогучин', 
    'Тольятти', 'Томари', 'Томмот', 'Томск', 'Топки', 'Торжок', 'Торопец', 'Тосно', 'Тотьма', 'Трехгорный', 
    'Трехгорный-1', 'Троицк', 'Троицк', 'Трубчевск', 'Туапсе', 'Туймазы', 'Тула', 'Тулун', 'Туран', 'Туринск', 
    'Тутаев', 'Тында', 'Тырныауз', 'Тюкалинск', 'Тюмень', 'Уварово', 'Углегорск', 'Углич', 'Удачный', 'Удомля', 
    'Ужур', 'Узловая', 'Улан-Удэ', 'Ульяновск', 'Унеча', 'Урай', 'Урень', 'Уржум', 'Урус-Мартан', 'Урюпинск', 
    'Усинск', 'Усмань', 'Усолье', 'Усолье-Сибирское', 'Уссурийск', 'Усть-Джегута', 'Усть-Илимск', 'Усть-Катав', 'Усть-Кут', 'Усть-Лабинск', 
    'Устюжна', 'Уфа', 'Ухта', 'Учалы', 'Уяр', 'Фатеж', 'Феодосия', 'Фокино', 'Фокино', 'Фролово', 
    'Фрязино', 'Фурманов', 'Хабаровск', 'Хадыженск', 'Ханты-Мансийск', 'Харабали', 'Харовск', 'Хасавюрт', 'Хвалынск', 'Хилок', 
    'Химки', 'Холм', 'Холмск', 'Хотьково', 'Цивильск', 'Цимлянск', 'Чадан', 'Чайковский', 'Чапаевск', 'Чаплыгин', 
    'Чебаркуль', 'Чебоксары', 'Чегем', 'Чекалин', 'Челябинск', 'Чердынь', 'Черемхово', 'Черепаново', 'Череповец', 'Черкесск', 
    'Чермоз', 'Черноголовка', 'Черногорск', 'Чернушка', 'Черняховск', 'Чехов', 'Чехов-2', 'Чехов-3', 'Чехов-8', 'Чистополь', 
    'Чита', 'Чкаловск', 'Чудово', 'Чулым', 'Чулым-3', 'Чусовой', 'Чухлома', 'Шагонар', 'Шадринск', 'Шали', 
    'Шарыпово', 'Шарья', 'Шатура', 'Шахтерск', 'Шахты', 'Шахунья', 'Шацк', 'Шебекино', 'Шелехов', 'Шенкурск', 
    'Шилка', 'Шимановск', 'Шиханы', 'Шлиссельбург', 'Шумерля', 'Шумиха', 'Шуя', 'Щекино', 'Щелкино', 'Щелково', 
    'Щербинка', 'Щигры', 'Щучье', 'Электрогорск', 'Электросталь', 'Электроугли', 'Элиста', 'Энгельс', 'Энгельс-19', 'Энгельс-2', 
    'Эртиль', 'Юбилейный', 'Югорск', 'Южа', 'Южно-Сахалинск', 'Южно-Сухокумск', 'Южноуральск', 'Юрга', 'Юрьевец', 'Юрьев-Польский', 
    'Юрюзань', 'Юхнов', 'Юхнов-1', 'Юхнов-2', 'Ядрин', 'Якутск', 'Ялта', 'Ялуторовск', 'Янаул', 'Яранск', 
    'Яровое', 'Ярославль', 'Ярцево', 'Ясногорск', 'Ясный', 'Яхрома'})

# SIMPLE = dictionary({
#     'москва',
#     'новосибирск',
#     'екатеринбург',
#     'казань',
#     'самара',
#     'омск',
#     'челябинск',
#     'уфа',
#     'волгоград',
#     'пермь',
#     'красноярск',
#     'воронеж',
#     'саратов',
#     'краснодар',
#     'тольятти',
#     'барнаул',
#     'ижевск',
#     'ульяновск',
#     'владивосток',
#     'ярославль',
#     'иркутск',
#     'тюмень',
#     'махачкала',
#     'хабаровск',
#     'оренбург',
#     'новокузнецк',
#     'кемерово',
#     'рязань',
#     'томск',
#     'астрахань',
#     'пенза',
#     'липецк',
#     'тула',
#     'киров',
#     'чебоксары',
#     'калининград',
#     'брянск',
#     'курск',
#     'иваново',
#     'магнитогорск',
#     'тверь',
#     'ставрополь',
#     'симферополь',
#     'белгород',
#     'архангельск',
#     'владимир',
#     'севастополь',
#     'сочи',
#     'курган',
#     'смоленск',
#     'калуга',
#     'чита',
#     'орёл',
#     'волжский',
#     'череповец',
#     'владикавказ',
#     'мурманск',
#     'сургут',
#     'вологда',
#     'саранск',
#     'тамбов',
#     'стерлитамак',
#     'грозный',
#     'якутск',
#     'кострома',
#     'петрозаводск',
#     'таганрог',
#     'нижневартовск',
#     'братск',
#     'новороссийск',
#     'дзержинск',
#     'шахта',
#     'нальчик',
#     'орск',
#     'сыктывкар',
#     'нижнекамск',
#     'ангарск',
#     'балашиха',
#     'благовещенск',
#     'прокопьевск',
#     'химки',
#     'псков',
#     'бийск',
#     'энгельс',
#     'рыбинск',
#     'балаково',
#     'северодвинск',
#     'армавир',
#     'подольск',
#     'королёв',
#     'сызрань',
#     'норильск',
#     'златоуст',
#     'мытищи',
#     'люберцы',
#     'волгодонск',
#     'новочеркасск',
#     'абакан',
#     'находка',
#     'уссурийск',
#     'березники',
#     'салават',
#     'электросталь',
#     'миасс',
#     'первоуральск',
#     'рубцовск',
#     'альметьевск',
#     'ковровый',
#     'коломна',
#     'керчь',
#     'майкоп',
#     'пятигорск',
#     'одинцово',
#     'копейск',
#     'хасавюрт',
#     'новомосковск',
#     'кисловодск',
#     'серпухов',
#     'новочебоксарск',
#     'нефтеюганск',
#     'димитровград',
#     'нефтекамск',
#     'черкесск',
#     'дербент',
#     'камышин',
#     'невинномысск',
#     'красногорск',
#     'мур',
#     'батайск',
#     'новошахтинск',
#     'ноябрьск',
#     'кызыл',
#     'октябрьский',
#     'ачинск',
#     'северск',
#     'новокуйбышевск',
#     'елец',
#     'евпатория',
#     'арзамас',
#     'обнинск',
#     'каспийск',
#     'элиста',
#     'пушкино',
#     'жуковский',
#     'междуреченск',
#     'сарапул',
#     'ессентуки',
#     'воткинск',
#     'ногинск',
#     'тобольск',
#     'ухта',
#     'серов',
#     'бердск',
#     'мичуринск',
#     'киселёвск',
#     'новотроицк',
#     'зеленодольск',
#     'соликамск',
#     'раменский',
#     'домодедово',
#     'магадан',
#     'глазов',
#     'железногорск',
#     'канск',
#     'назрань',
#     'гатчина',
#     'саров',
#     'новоуральск',
#     'воскресенск',
#     'долгопрудный',
#     'бугульма',
#     'кузнецк',
#     'губкин',
#     'кинешма',
#     'ейск',
#     'реутов',
#     'железногорск',
#     'чайковский',
#     'азов',
#     'бузулук',
#     'озёрск',
#     'балашов',
#     'юрга',
#     'кропоткин',
#     'клин'
# })

GOROD_ABBR = in_caseless({
    'спб',
    'мск',
    'нск'   # Новосибирск
})

GOROD_NAME = or_(
    #rule(SIMPLE),
    #COMPLEX,
    CITIES_TOWNS,
    rule(GOROD_ABBR)
).interpretation(
    Settlement.name
)

SIMPLE = and_(
    TITLE,
    or_(
        NOUN,
        ADJF  # Железнодорожный, Юбилейный
    )
)

COMPLEX = or_(
    rule(
        SIMPLE,
        DASH.optional(),
        SIMPLE
    ),
    rule(
        TITLE,
        DASH.optional(),
        caseless('на'),
        DASH.optional(),
        TITLE
    )
)

NAME = or_(
    rule(SIMPLE),
    COMPLEX
)

MAYBE_GOROD_NAME = or_(
    NAME,
    rule(NAME, '-', INT)
).interpretation(
    Settlement.name
)

GOROD_WORDS = or_(
    rule(normalized('город')),
    rule(
        caseless('г'),
        DOT.optional()
    )
).interpretation(
    Settlement.type.const('город')
)

GOROD = or_(
    rule(GOROD_WORDS, MAYBE_GOROD_NAME),
    rule(
        GOROD_WORDS.optional(),
        GOROD_NAME
    )
).interpretation(
    Settlement
)


##########
#
#  SETTLEMENT NAME
#
##########


ADJS = gram('ADJS')
SIMPLE = and_(
    # or_(
    #     NOUN,  # Александровка, Заречье, Горки
    #     ADJS,  # Кузнецово
    #     ADJF,  # Никольское, Новая, Марьино
    # ),
    TITLE
)

COMPLEX = rule(
    SIMPLE,
    DASH.optional(),
    SIMPLE
)

NAME = or_(
    rule(SIMPLE),
    COMPLEX
)

SETTLEMENT_NAME = or_(
    NAME,
    rule(NAME, '-', INT),
    rule(NAME, ANUM)
)


###########
#
#   SELO
#
#############


SELO_WORDS = or_(
    rule(
        caseless('с'),
        DOT.optional()
    ),
    rule(
        caseless('с'), #сельское поселение
        DOT.optional(),
        caseless('п'),
        DOT.optional()
    ),
    rule(normalized('село'))
).interpretation(
    Settlement.type.const('село')
)

SELO_NAME = SETTLEMENT_NAME.interpretation(
    Settlement.name
)

SELO = rule(
    SELO_WORDS,
    SELO_NAME
).interpretation(
    Settlement
)


###########
#
#   DEREVNYA
#
#############


DEREVNYA_WORDS = or_(
    rule(
        caseless('д'),
        DOT.optional()
    ),
    rule(normalized('деревня'))
).interpretation(
    Settlement.type.const('деревня')
)

DEREVNYA_NAME = SETTLEMENT_NAME.interpretation(
    Settlement.name
)

DEREVNYA = rule(
    DEREVNYA_WORDS,
    DEREVNYA_NAME
).interpretation(
    Settlement
)


###########
#
#   POSELOK
#
#############


POSELOK_WORDS = or_(
    rule(
        in_caseless({'п', 'пос'}),
        DOT.optional()
    ),
    rule(normalized('посёлок')),
    rule(
        caseless('р'),
        DOT.optional(),
        caseless('п'),
        DOT.optional()
    ),
    rule(
        normalized('рабочий'),
        normalized('посёлок')
    ),
    rule(
        caseless('пгт'),
        DOT.optional()
    ),
    rule(
        caseless('п'), DOT, caseless('г'), DOT, caseless('т'),
        DOT.optional()
    ),
    rule(
        normalized('посёлок'),
        normalized('городского'),
        normalized('типа'),
    ),
).interpretation(
    Settlement.type.const('посёлок')
)

POSELOK_NAME = SETTLEMENT_NAME.interpretation(
    Settlement.name
)

POSELOK = rule(
    POSELOK_WORDS,
    POSELOK_NAME
).interpretation(
    Settlement
)


##############
#
#   ADDR PERSON
#
############


ABBR = and_(
    length_eq(1),
    is_title()
)

PART = and_(
    TITLE,
    or_(
        gram('Name'),
        gram('Surn')
    )
)

MAYBE_FIO = or_(
    rule(TITLE, PART),
    rule(PART, TITLE),
    rule(ABBR, '.', TITLE),
    rule(ABBR, '.', ABBR, '.', TITLE),
    rule(TITLE, ABBR, '.', ABBR, '.')
)

POSITION_WORDS_ = or_(
    rule(
        dictionary({
            'мичман',
            'геолог',
            'подводник',
            'краевед',
            'снайпер',
            'штурман',
            'бригадир',
            'учитель',
            'политрук',
            'военком',
            'ветеран',
            'историк',
            'пулемётчик',
            'авиаконструктор',
            'адмирал',
            'академик',
            'актер',
            'актриса',
            'архитектор',
            'атаман',
            'врач',
            'воевода',
            'генерал',
            'губернатор',
            'хирург',
            'декабрист',
            'разведчик',
            'граф',
            'десантник',
            'конструктор',
            'скульптор',
            'писатель',
            'поэт',
            'капитан',
            'князь',
            'комиссар',
            'композитор',
            'космонавт',
            'купец',
            'лейтенант',
            'лётчик',
            'майор',
            'маршал',
            'матрос',
            'подполковник',
            'полковник',
            'профессор',
            'сержант',
            'старшина',
            'танкист',
            'художник',
            'герой',
            'княгиня',
            'строитель',
            'дружинник',
            'диктор',
            'прапорщик',
            'артиллерист',
            'графиня',
            'большевик',
            'патриарх',
            'сварщик',
            'офицер',
            'рыбак',
            'брат',
        })
    ),
    rule(normalized('генерал'), normalized('армия')),
    rule(normalized('герой'), normalized('россия')),
    rule(
        normalized('герой'),
        normalized('российский'), normalized('федерация')),
    rule(
        normalized('герой'),
        normalized('советский'), normalized('союз')
    ),
)

ABBR_POSITION_WORDS = rule(
    in_caseless({
        'адм',
        'ак',
        'акад',
    }),
    DOT.optional()
)

POSITION_WORDS = or_(
    POSITION_WORDS_,
    ABBR_POSITION_WORDS
)

MAYBE_PERSON = or_(
    MAYBE_FIO,
    rule(POSITION_WORDS, MAYBE_FIO),
    rule(POSITION_WORDS, TITLE)
)


###########
#
#   IMENI
#
##########


IMENI_WORDS = or_(
    rule(
        caseless('им'),
        DOT.optional()
    ),
    rule(caseless('имени'))
)

IMENI = or_(
    rule(
        IMENI_WORDS.optional(),
        MAYBE_PERSON
    ),
    rule(
        IMENI_WORDS,
        TITLE
    )
)

##########
#
#   LET
#
##########


LET_WORDS = or_(
    rule(caseless('лет')),
    rule(
        DASH.optional(),
        caseless('летия')
    )
)

LET_NAME = in_caseless({
    'влксм',
    'ссср',
    'алтая',
    'башкирии',
    'бурятии',
    'дагестана',
    'калмыкии',
    'колхоза',
    'комсомола',
    'космонавтики',
    'москвы',
    'октября',
    'пионерии',
    'победы',
    'приморья',
    'района',
    'совхоза',
    'совхозу',
    'татарстана',
    'тувы',
    'удмуртии',
    'улуса',
    'хакасии',
    'целины',
    'чувашии',
    'якутии',
})

LET = rule(
    INT,
    LET_WORDS,
    LET_NAME
)


##########
#
#    ADDR DATE
#
#############


MONTH_WORDS = dictionary({
    'январь',
    'февраль',
    'март',
    'апрель',
    'май',
    'июнь',
    'июль',
    'август',
    'сентябрь',
    'октябрь',
    'ноябрь',
    'декабрь',
})

DAY = and_(
    INT,
    gte(1),
    lte(31)
)

YEAR = and_(
    INT,
    gte(1),
    lte(2100)
)

YEAR_WORDS = normalized('год')

DATE = or_(
    rule(DAY, MONTH_WORDS),
    rule(YEAR, YEAR_WORDS)
)


#########
#
#   MODIFIER
#
############


MODIFIER_WORDS_ = rule(
    dictionary({
        'большой',
        'малый',
        'средний',

        'верхний',
        'центральный',
        'нижний',
        'северный',
        'дальний',

        'первый',
        'второй',

        'старый',
        'новый',

        'красный',
        'лесной',
        'тихий',
    }),
    DASH.optional()
)

ABBR_MODIFIER_WORDS = rule(
    in_caseless({
        'б', 'м', 'н'
    }),
    DOT.optional()
)

SHORT_MODIFIER_WORDS = rule(
    in_caseless({
        'больше',
        'мало',
        'средне',

        'верх',
        'верхне',
        'центрально',
        'нижне',
        'северо',
        'дальне',
        'восточно',
        'западно',

        'перво',
        'второ',

        'старо',
        'ново',

        'красно',
        'тихо',
        'горно',
    }),
    DASH.optional()
)

MODIFIER_WORDS = or_(
    MODIFIER_WORDS_,
    ABBR_MODIFIER_WORDS,
    SHORT_MODIFIER_WORDS,
)


##########
#
#   ADDR NAME
#
##########


ROD = gram('gent')

SIMPLE = and_(
    or_(
        ADJF,  # Школьная
        and_(NOUN, ROD),  # Ленина, Победы
    ),
    TITLE
)

COMPLEX = or_(
    rule(
        and_(ADJF, TITLE),
        NOUN
    ),
    rule(
        TITLE,
        DASH.optional(),
        TITLE
    ),
)

# TODO
EXCEPTION = dictionary({
    'арбат',
    'варварка'
})

MAYBE_NAME = or_(
    rule(SIMPLE),
    COMPLEX,
    rule(EXCEPTION)
)

NAME = or_(
    MAYBE_NAME,
    LET,
    DATE,
    IMENI
)

NAME = rule(
    MODIFIER_WORDS.optional(),
    NAME
)

ADDR_CRF = tag('I').repeatable()

NAME = or_(
    NAME,
    ANUM,
    rule(NAME, ANUM),
    rule(ANUM, NAME),
    rule(INT, DASH.optional(), NAME),
    rule(NAME, DASH, INT),
    ADDR_CRF
)

ADDR_NAME = NAME


########
#
#    STREET
#
#########


STREET_WORDS = or_(
    rule(normalized('улица')),
    rule(
        caseless('ул'),
        DOT.optional()
    )
).interpretation(
    Street.type.const('улица')
)

STREET_NAME = ADDR_NAME.interpretation(
    Street.name
)

STREET = or_(
    rule(STREET_WORDS, STREET_NAME),
    rule(STREET_NAME, STREET_WORDS)
).interpretation(
    Street
)


##########
#
#    PROSPEKT
#
##########


PROSPEKT_WORDS = or_(
    rule(
        in_caseless({'пр', 'просп', 'пр-кт', 'пр-т'}),
        DOT.optional()
    ),
    rule(normalized('проспект'))
).interpretation(
    Street.type.const('проспект')
)

PROSPEKT_NAME = ADDR_NAME.interpretation(
    Street.name
)

PROSPEKT = or_(
    rule(PROSPEKT_WORDS, PROSPEKT_NAME),
    rule(PROSPEKT_NAME, PROSPEKT_WORDS)
).interpretation(
    Street
)


############
#
#    PROEZD
#
#############


PROEZD_WORDS = or_(
    rule(caseless('пр'), DOT.optional()),
    rule(
        caseless('пр'),
        '-',
        in_caseless({'зд', 'д'}),
        DOT.optional()
    ),
    rule(normalized('проезд'))
).interpretation(
    Street.type.const('проезд')
)

PROEZD_NAME = ADDR_NAME.interpretation(
    Street.name
)

PROEZD = or_(
    rule(PROEZD_WORDS, PROEZD_NAME),
    rule(PROEZD_NAME, PROEZD_WORDS)
).interpretation(
    Street
)


###########
#
#   PEREULOK
#
##############


PEREULOK_WORDS = or_(
    rule(
        caseless('п'),
        DOT
    ),
    rule(
        caseless('пер'),
        DOT.optional()
    ),
    rule(normalized('переулок'))
).interpretation(
    Street.type.const('переулок')
)

PEREULOK_NAME = ADDR_NAME.interpretation(
    Street.name
)

PEREULOK = or_(
    rule(PEREULOK_WORDS, PEREULOK_NAME),
    rule(PEREULOK_NAME, PEREULOK_WORDS)
).interpretation(
    Street
)


########
#
#  PLOSHAD
#
##########


PLOSHAD_WORDS = or_(
    rule(
        caseless('пл'),
        DOT.optional()
    ),
    rule(normalized('площадь'))
).interpretation(
    Street.type.const('площадь')
)

PLOSHAD_NAME = ADDR_NAME.interpretation(
    Street.name
)

PLOSHAD = or_(
    rule(PLOSHAD_WORDS, PLOSHAD_NAME),
    rule(PLOSHAD_NAME, PLOSHAD_WORDS)
).interpretation(
    Street
)


############
#
#   SHOSSE
#
###########


# TODO
# Покровское 17 км.
# Сергеляхское 13 км
# Сергеляхское 14 км.


SHOSSE_WORDS = or_(
    rule(
        caseless('ш'),
        DOT
    ),
    rule(normalized('шоссе'))
).interpretation(
    Street.type.const('шоссе')
)

SHOSSE_NAME = ADDR_NAME.interpretation(
    Street.name
)

SHOSSE = or_(
    rule(SHOSSE_WORDS, SHOSSE_NAME),
    rule(SHOSSE_NAME, SHOSSE_WORDS)
).interpretation(
    Street
)


########
#
#  NABEREG
#
##########


NABEREG_WORDS = or_(
    rule(
        caseless('наб'),
        DOT.optional()
    ),
    rule(normalized('набережная'))
).interpretation(
    Street.type.const('набережная')
)

NABEREG_NAME = ADDR_NAME.interpretation(
    Street.name
)

NABEREG = or_(
    rule(NABEREG_WORDS, NABEREG_NAME),
    rule(NABEREG_NAME, NABEREG_WORDS)
).interpretation(
    Street
)


########
#
#  BULVAR
#
##########


BULVAR_WORDS = or_(
    rule(
        caseless('б'),
        '-',
        caseless('р')
    ),
    rule(
        caseless('б'),
        DOT
    ),
    rule(
        caseless('бул'),
        DOT.optional()
    ),
    rule(normalized('бульвар'))
).interpretation(
    Street.type.const('бульвар')
)

BULVAR_NAME = ADDR_NAME.interpretation(
    Street.name
)

BULVAR = or_(
    rule(BULVAR_WORDS, BULVAR_NAME),
    rule(BULVAR_NAME, BULVAR_WORDS)
).interpretation(
    Street
)


##############
#
#   ADDR VALUE
#
#############


LETTER = in_caseless(set('абвгдежзиклмнопрстуфхшщэюя'))

QUOTE = in_(QUOTES)

LETTER = or_(
    rule(LETTER),
    rule(QUOTE, LETTER, QUOTE),
    rule(
        eq(',').optional(),
        in_caseless({
            'литера',
            'литер',
            'лит'
        }),
        DOT.optional(),
        or_(
            rule(LETTER),
            rule(QUOTE, LETTER, QUOTE)
        )
    )
)

VALUE = rule(
    or_(
        INT,
        in_caseless({'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV'})
    ),
    LETTER.optional()
)

SEP = in_(r'/\-')

VALUE = or_(
    rule(VALUE),
    rule(VALUE, SEP, VALUE),
    rule(VALUE, SEP, LETTER)
)

ADDR_VALUE = rule(
    eq('№').optional().repeatable(),
    VALUE
)


############
#
#    DOM
#
#############


DOM_WORDS = or_(
    rule(normalized('дом')),
    rule(
        caseless('д'),
        DOT
    )
).interpretation(
    Building.type.const('дом')
)

DOM_VALUE = ADDR_VALUE.interpretation(
    Building.number
)

DOM = rule(
    DOM_WORDS.optional(),
    DOM_VALUE
).interpretation(
    Building
)


###########
#
#  KORPUS
#
##########


KORPUS_WORDS = or_(
    rule(
        in_caseless({'корп', 'кор'}),
        DOT.optional()
    ),
    rule(normalized('корпус'))
).interpretation(
    Building.type.const('корпус')
)

KORPUS_VALUE = ADDR_VALUE.interpretation(
    Building.number
)

KORPUS = or_(
    rule(
        KORPUS_WORDS,
        KORPUS_VALUE
    ),
    rule(
        KORPUS_VALUE,
        KORPUS_WORDS
    )
).interpretation(
    Building
)


###########
#
#  STROENIE
#
##########


STROENIE_WORDS = or_(
    rule(
        caseless('стр'),
        DOT.optional()
    ),
    rule(
        caseless('строен'),
        DOT.optional()
    ),
    rule(normalized('строение'))
).interpretation(
    Building.type.const('строение')
)

STROENIE_VALUE = ADDR_VALUE.interpretation(
    Building.number
)

STROENIE = rule(
    STROENIE_WORDS,
    STROENIE_VALUE
).interpretation(
    Building
)


###########
#
#   OFIS
#
#############


OFIS_WORDS = or_(
    rule(
        caseless('оф'),
        DOT.optional()
    ),
    rule(normalized('офис'))
).interpretation(
    Room.type.const('офис')
)

OFIS_VALUE = ADDR_VALUE.interpretation(
    Room.number
)

OFIS = rule(
    OFIS_WORDS,
    OFIS_VALUE
).interpretation(
    Room
)


###########
#
#   KVARTIRA
#
#############


KVARTIRA_WORDS = or_(
    rule(caseless('кв'), DOT.optional()),
    rule(caseless('к'), DOT.optional()),
    rule(normalized('квартира')),
    rule(normalized('комната')),
    rule(caseless('в'), caseless('комнате')),
    rule(caseless('часть'), caseless('комнаты'))
).interpretation(
    Room.type.const('квартира')
)

KVARTIRA_VALUE = ADDR_VALUE.interpretation(
    Room.number
)

KVARTIRA = rule(
    KVARTIRA_WORDS,
    KVARTIRA_VALUE
).interpretation(
    Room
)

QUARTAL = or_(
    rule(
        caseless('в').optional(),
        normalized('квартал'),
        ADDR_VALUE
    ),
    rule(
        ADDR_VALUE,
        normalized('квартал')
    )
)

########

ADDR_VALUE_FLOOR = or_(
    rule(INT),
    rule(INT, eq('-'), or_(caseless('го'), caseless('ый'))),
    rule(gram('Anum'))
)

FLOOR = or_(
    rule(caseless('подвал')),
    rule(ADDR_VALUE_FLOOR, normalized('этаж'), eq(':').optional()),
    rule(normalized('этаж'), ADDR_VALUE_FLOOR),
    rule(caseless('на'), ADDR_VALUE_FLOOR, caseless('этаже'), eq(':').optional()),
)

def is_address_token_with_commas(word):
  match = re.compile('[,-a-zA-Zа-яёА-ЯЁ.0-9"«»№]+').match(word)
  if match is None:
    return False
  return match.span()[1] == len(word)

SOMETHING_WITH_COMMAS = rule(
    custom(is_address_token_with_commas).repeatable()
)

SOMETHING_IN_BRACKETS = rule(
    eq('('),
    #SOMETHING.repeatable(),
    SOMETHING_WITH_COMMAS,
    eq(')')
)

def is_maybe_number(word):
  match = re.compile('(№)?[-0-9]*').match(word)
  if match is None:
    return False
  return match.span()[1] == len(word)

ADDR_VALUE_EXTENDED = rule(
    custom(is_maybe_number).repeatable()
)

POMESCHENIE = rule(
    or_(
        rule(caseless('пом'), DOT.optional(), eq(':').optional()),
        rule(caseless('н'), DOT, caseless('п'), DOT),
        rule(normalized('часть').optional(), or_(ADJF, gram('Anum'), gram('PRTF')).optional().repeatable(), normalized('помещение'), eq(':').optional())
    ).interpretation(
        Room.type.const('помещение')
    ),
    or_(
        ADDR_VALUE.interpretation(
            Room.number
        ),
        ADDR_VALUE_EXTENDED
    ).optional()
).interpretation(
    Room
)

DOM_WORDS = or_(
    rule(normalized('дом')),
    rule(caseless('д'), DOT),
    rule(caseless('вл'), DOT)
).interpretation(
    Building.type.const('дом')
)

DOM_VALUE = ADDR_VALUE.interpretation(
    Building.number
)


###########
#
#   INDEX
#
#############


INDEX = and_(
    INT,
    gte(100000),
    lte(999999)
).interpretation(
    Index.value
).interpretation(
    Index
)


#############
#
#   ADDR PART
#
############

# AddressInterpretation = fact(
#     'Address',
#     [attribute('titles').repeatable()]
# )

# ADDR_PART = or_(
#     INDEX,
#     COUNTRY,
#     FED_OKRUG,

#     RESPUBLIKA,
#     KRAI,
#     OBLAST,
#     AUTO_OKRUG,

#     RAION,
#     GOROD,
#     DEREVNYA,
#     SELO,
#     POSELOK,

#     STREET,
#     PROSPEKT,
#     PROEZD,
#     PEREULOK,
#     PLOSHAD,
#     SHOSSE,
#     NABEREG,
#     BULVAR,

#     DOM,
#     KORPUS,
#     STROENIE,
#     OFIS,
#     KVARTIRA
# ).interpretation(
#     AddrPart.value
# ).interpretation(
#     AddrPart
# )

# class FixedAddrExtractor(AddrExtractor):
#     def __init__(self, morph):
#         Extractor.__init__(self, ADDR_PART, morph)

#ТЕСТ
# from natasha import AddrExtractor, MorphVocab
# extractor = FixedAddrExtractor(MorphVocab())
# matches = extractor('район Комсомольский ул Светлая')
# #print(*matches, sep = '\n')
# for m in matches:
#   display(m.tree.as_dot)

# LOCATION = _or(
#     equal('123')
# )

FULL_STREET_RULE = rule(
    or_(
        STREET, PROSPEKT, PROEZD, PEREULOK, PLOSHAD, SHOSSE, NABEREG, BULVAR
    ),
    eq(',').optional(),
)

FULL_HOUSE_RULE = rule(
    rule(
        or_(DOM, KORPUS, STROENIE, OFIS, KVARTIRA, POMESCHENIE, FLOOR, SOMETHING_IN_BRACKETS, QUARTAL),
        eq(',').optional()
    ),
    rule(
        or_(DOM, KORPUS, STROENIE, OFIS, KVARTIRA, POMESCHENIE, FLOOR, SOMETHING_IN_BRACKETS, QUARTAL, LETTER),
        eq(',').optional()
    ).optional().repeatable()
)

FROM_ADDRESS_TO_HOUSE = rule(
    rule(
        or_(
            COUNTRY,
            FED_OKRUG,
            RESPUBLIKA, KRAI, OBLAST, AUTO_OKRUG,
            RAION, GOROD, DEREVNYA, SELO, POSELOK
        ),
        eq(',').optional(),
        SOMETHING.optional(),
        eq(',').optional()
    ).repeatable(),
    or_(
        rule(FULL_STREET_RULE.repeatable(), FULL_HOUSE_RULE.optional()), #указана улица, дом не обязателен
        rule(FULL_STREET_RULE.repeatable().optional(), FULL_HOUSE_RULE) #указан дом, улица не обязательна
    )
)

ADDR_PART = rule(
    FROM_ADDRESS_TO_HOUSE
).interpretation(AddrPart)

from yargy import Parser
from yargy.tokenizer import MorphTokenizer, TokenRule

ADDR_TOKENIZER = MorphTokenizer() \
  .remove_types('RU').add_rules(TokenRule('RU', '[-а-яё]+')) \
  .remove_types('LATIN').add_rules(TokenRule('LATIN', '[-a-z]+'))
  #.remove_types('PUNCT').add_rules(TokenRule('PUNCT', '[\\\\/!#$%&()\\[\\]\\*\\+,\\.:;<=>?@^_`{|}~№…"\\\'«»„“ʼʻ”]')) \

# TEST_RULE = rule(
#         or_(DOM, KORPUS, STROENIE, OFIS, KVARTIRA, POMESCHENIE, FLOOR, SOMETHING_IN_BRACKETS, QUARTAL, LETTER),
#         eq(',').optional()
#     ).repeatable()

override = None #TEST_RULE #ADDR_VALUE_EXTENDED #PEREULOK #AUTO_OKRUG
ADDR_PARSER = Parser(override if override else ADDR_PART, tokenizer = ADDR_TOKENIZER)

#функция предобработки: возвращает измененный текст и массив из пар (спан в измененном тексте, исходная последовательность символов), каждый код из словаря встречается в тексте ровно один раз
number_pattern = r'((№\s+)?\d+(-\d+)?(\/\d+)?[а-я]?)'
pattern = re.compile(fr'{number_pattern}([,\s]+{number_pattern})+')
min_length = 3
def replace_digits_sequences(text):
  replaced_text_parts = []
  index_shift = 0
  code_index = 0
  for x in re.finditer(pattern, text.lower()):
    start, end = x.span()
    if end - start < min_length: continue
    start += index_shift
    end += index_shift
    code = '9' + f'{code_index:03o}' + '9' #девятки с концов, внутри нет девяток, чтобы не случилось коллизий
    code_index += 1
    orig_text = text[start:end]
    text = text[:start] + code + text[end:]
    replaced_text_parts.append(((start, start + len(code)), orig_text))
    index_shift += len(code) - len(orig_text)
  return text, replaced_text_parts

#тест функции предобработки
text_preprocessed, replaced_text_parts = replace_digits_sequences('Арбитражного Суда неоднократно Краснодарский край, г. Сочи, Лазаревский район,  ул. Сибирская, д. 16, литер А5, помещения № 19/1,19/2,19/3,19/4 разъяснял')
print(replaced_text_parts)
for (start, end), orig_text in replaced_text_parts:
  print(text_preprocessed[start:end], orig_text)

def get_ADDR_PARSER_spans(text):
  matches = list(ADDR_PARSER.findall(text)) #matches содержат спаны, формы слов и пр. (объекты Match)
  match_spans = [(m.span.start, m.span.stop) for m in matches] #match_spans содержат только спаны (пары начало-конец)
  return match_spans

from yargy.span import Span
def find_address_spans(text): #принимает текст, возвращает массив спанов: [(начало, конец), ...]
  #return get_ADDR_PARSER_spans(text) #без пред- и постобработки
  #предобработка
  text_preprocessed, replaced_text_parts = replace_digits_sequences(text)
  #поиск адресов
  match_spans = get_ADDR_PARSER_spans(text_preprocessed)
  #постобработка
  def is_inside(span1, span2): #возвращает 'yes' если span1 целиком внутри span2, 'no' если целиком снаружи, 'partly' если частично внутри
    if span1[0] >= span2[0] and span1[1] <= span2[1]: return 'yes'
    if span1[1] <= span2[0] or span1[0] >= span2[1]: return 'no'
    return 'partly'
  def shift(span, delta): #сдвигаем спан на дельту
    return (span[0] + delta, span[1] + delta)
  #перебираем спаны и коды, чтобы изменить границы спанов так, чтобы они соответствовали исходному тексту
  for match_span_idx, (match_start, match_end) in enumerate(match_spans):
    prev_match_end = match_spans[match_span_idx - 1][1] if match_span_idx > 0 else 0
    for code_span, orig_text in replaced_text_parts:
      delta_for_this_code = len(orig_text) - (code_span[1] - code_span[0])
      code_before_current_span = is_inside(code_span, (prev_match_end, match_start))
      code_inside_current_span = is_inside(code_span, (match_start, match_end))
      if code_before_current_span == 'yes' or code_inside_current_span == 'yes':
        if code_inside_current_span == 'yes': #смещаем конец текущего спана
          match_spans[match_span_idx] = (match_start, match_end + delta_for_this_code)
        #смещаем все спаны после текущего
        for i in range(match_span_idx + 1, len(match_spans)):
            match_spans[i] = shift(match_spans[i], delta_for_this_code)
      elif code_before_current_span == 'partly' or code_inside_current_span == 'partly':
        #оригинальный текст мы заменили кодом, но он оказался разбит границей одного из матчей
        #в этом случае мы повторяем распознавание без предобработки
        print('tried to replace original text by code, but match divides the code into parts; trying without replacing...')
        return get_match_spans(text)
  return match_spans #matches
