#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
make_facets.py — строит /data/registry_facets.json для фильтров атласа.

Зачем отдельный файл: registry_index.json перезаписывается вашим
sync_registry.sh, поэтому признаки для фильтров лежат рядом, а не внутри
индекса. После каждого обновления реестра прогоните этот скрипт заново.

Запуск:
    python3 make_facets.py reg/ facets.json
где reg/ — каталог с registry_<код>.json (или А.json, Б.json … — берутся
все *.json, содержащие словарь id -> запись).
"""
import json, os, re, sys

STRUCT = [
    (re.compile(r'мельниц\w*', re.I), 'мельница'),
    (re.compile(r'фабрик\w*', re.I), 'фабрика'),
    (re.compile(r'завод\w*', re.I), 'завод'),
    (re.compile(r'кузниц\w*', re.I), 'кузница'),
    (re.compile(r'винокур\w*', re.I), 'винокурня'),
    (re.compile(r'маслобойн\w*', re.I), 'маслобойня'),
    (re.compile(r'солодовн\w*', re.I), 'солодовня'),
    (re.compile(r'кирпичн\w*', re.I), 'кирпичный завод'),
    (re.compile(r'мануфактур\w*', re.I), 'мануфактура'),
    (re.compile(r'(церковь|церкви|собор\w*|храм\w*)', re.I), 'церковь'),
    (re.compile(r'(господск\w*|усадьб\w*)', re.I), 'господский дом'),
    (re.compile(r'пруд\w*', re.I), 'пруд'),
]
PROD = {'мельница', 'фабрика', 'завод', 'кузница', 'винокурня',
        'маслобойня', 'солодовня', 'кирпичный завод', 'мануфактура'}

EPOHI = ['I', 'II', 'III', 'IV', 'V', 'VI']

PATR = re.compile(r'(ович|евич|овна|евна|ична|инична)$', re.I)
NAMES = """иван петр пётр василь михаил михайл александр алексе николай сергей григор
федор ѳедор андрей дмитр семен степан яков матве максим борис егор никифор афанас аѳонас
тимофе прокоф данил кирил амплей назар влас гаврил иль лев роман павел антон игнат фома
лука марк осип евграф гаврил ефим захар кузьм лаврент леонт лукьян мирон никит пахом
родион сава савва самойл тарас трофим устин филип харитон яким аким адриан авраам
анн мари марф прасков аграфен натал соф катерин екатерин авдоть ирин пелаге вер
александр дарь елен татьян анастас ксени матрен устинь агафь варвар евдок феодос
акулин анисьи домн ефросинь любов надежд олимпиад параскев глафир евгени клеопатр
слобод девиц вдов""".split()
NAMESET = set(NAMES)

SKIP_CHUNK = re.compile(r'церкв|собор|священ|монастыр|казенн|казённ|однодворц|'
                        r'экономическ|ямщик|обротн|оброчн|пустош|дьячк|причт', re.I)
ROLE = re.compile(r'^(сына|сыновей|детей|дочери|дочерей|жены|вдовы|девицы|его|ея|их|'
                  r'малолетн\w*|поручик\w*|капитан\w*|майор\w*|прапорщик\w*|секунд|'
                  r'князя|княгини|графа|графини|бригадир\w*|генерал\w*|подпоручик\w*|'
                  r'коллежск\w*|надворн\w*|статск\w*|асессор\w*|советник\w*|титулярн\w*)$', re.I)

def stem(w):
    w = w.rstrip('ъ')
    for a, b in (('ского','ский'),('скаго','ский'),('ской','ский'),('ская','ский'),
                 ('ских','ский'),('скія','ский'),('скій','ский')):
        if w.lower().endswith(a):
            return w[:-len(a)] + b
    for suf in ('овых','евых','ыхъ','ых','ой','ей','ою','ая','а','у','е','ы','и','ой'):
        if w.lower().endswith(suf) and len(w) - len(suf) >= 4:
            return w[:-len(suf)]
    return w

def owners(r):
    ep = r.get('ep') or {}
    clean = ep.get('vladelcy')
    src, first_is_surname = None, False
    if clean:
        src, first_is_surname = clean, True
    else:
        pr = r.get('primechanie') or ''
        pr = pr.split('▪')[0]
        m = re.search(r'владельц\w*(?:\s+по\s+ЭП)?\s*:\s*(.+)', pr, re.I)
        if m:
            src = m.group(1)
    if not src:
        return []
    src = src.split('||')[0]
    out = []
    for chunk in re.split(r'[;]| и (?=[А-ЯЁ])', src):
        chunk = chunk.strip(' .,')
        if not chunk or SKIP_CHUNK.search(chunk):
            continue
        words = re.findall(r'[А-ЯЁѲІ][а-яёѳі]{2,}', chunk)
        if not words:
            continue
        cands = [w for w in words if not ROLE.match(w)]
        if not cands:
            continue
        pick = cands[0] if first_is_surname else cands[-1]
        s = stem(pick)
        if len(s) < 4 or s.lower() in NAMESET or PATR.search(s):
            # генитив «Аграфены Ивановой дочери» — фамилии нет
            if not first_is_surname and len(cands) > 1:
                s2 = stem(cands[-2])
                if len(s2) >= 4 and s2.lower() not in NAMESET and not PATR.search(s2):
                    out.append(s2)
            continue
        out.append(s)
    seen, res = set(), []
    for w in out:
        if w.lower() in seen: continue
        seen.add(w.lower()); res.append(w)
    return res[:8]

def rec_text(r):
    ep = r.get('ep') or {}
    parts = [ep.get('stroeniya_zav'), r.get('doslovno'), ep.get('primechanie'),
             ep.get('zvanie'), ep.get('stroeniya'), r.get('alfavit')]
    return ' '.join(p for p in parts if p)


def facet(r, rid=''):
    t = rec_text(r)
    labels = sorted({lb for rx, lb in STRUCT if rx.search(t)})
    ep = r.get('ep') or {}
    epo = r.get('epohi') or {}
    mask = 0
    for i, k in enumerate(EPOHI):
        if epo.get(k):
            mask |= 1 << i
    f = {}
    if r.get('flags'):
        f['fl'] = r['flags']
    if r.get('propavshie'):
        f['pr'] = 1
    if r.get('sobytie'):
        f['so'] = 1
    if labels:
        f['sb'] = labels
    if any(l in PROD for l in labels):
        f['pd'] = 1
    ow = owners(r)
    if ow:
        f['vl'] = ow
    try:
        dv = int(float(ep.get('dvorov')))
        if dv:
            f['dv'] = dv
    except (TypeError, ValueError):
        pass
    gd = [g for g in (r.get('gidronimy') or []) if g]
    if gd:
        f['gd'] = gd[:8]
    if mask:
        f['ep'] = mask
    # след в писцовых книгах: жилое / уже пустошь, что была деревня / просто упоминание.
    # Нужен значку на карте, поэтому кладём готовым признаком, а не текстом.
    e1 = str((r.get('epohi') or {}).get('I') or '')
    listy = (r.get('pk') or {}).get('listy')
    kind = str(r.get('kind') or '')
    if re.search(r'что\s*был', e1 + ' ' + kind, re.I):
        f['pk'] = 'was'
    elif re.search(r'деревня|село|сельцо|починок', e1, re.I) and 'пустош' not in e1.lower():
        f['pk'] = 'live'
    elif e1 or listy:
        f['pk'] = 'trace'
    if listy:
        f['pkl'] = 1

    # Когда исчезло относительно межевания — главное для краеведа деление.
    #   was  — в писцовой книге уже «пустошь, что была деревня»: пропало до 1620-х
    #   gone — жилое в ПК, а в межевании уже пустошь: пропало между 1628 и 1780
    #   ep   — «что была деревня» знает только межевание
    # Ценность в том, что на таких местах нет позднейшей застройки.
    ep = r.get('epohi') or {}
    # «пустошь» может стоять не только в межевании, но и у Кёппена,
    # на трёхвёрстке, в Списках населённых мест — берём все поздние срезы
    later = ' '.join(str(ep.get(k) or '') for k in ('II', 'III', 'IV', 'V', 'VI'))
    e2 = str(ep.get('II') or '')
    # Оборот «что была…» пишется по-разному: полным словом, сокращением
    # «что была д.», «что было с.», «что была сц.», и аббревиатурой
    # «пчбд» — пустошь, что была деревня. Исчезнуть могла не только
    # деревня, но и село, сельцо, починок, слобода, погост.
    byla = re.compile(
        r'что\s*был[аои]?\s*'
        r'(деревн\w*|де-ревня|сельц\w*|сел[оа]\b|селом|починок|починк\w*|'
        r'слобод\w*|погост\w*|д\.|дер\.|с\.|сц\.|сел\.|пуст\w*)'
        r'|пустош[ьяи]?,?\s*что\s*был'
        r'|\bп\.?\s?ч\.?\s?б\.?\s?(д|с|сц)\.?(?![а-яё])',
        re.I)
    zhil = re.compile(r'(^|▪|;)\s*(деревня|село|сельцо|починок)', re.I)
    pust = re.compile(r'пустош', re.I)
    # писцовая книга 1650 г. — второй, более поздний срез XVII века
    # ------------------------------------------------------------------
    # Привязка к книге 1650 г. бывает предположительной, и тогда в графе
    # стоит запись СОВСЕМ ДРУГОГО селения: составитель перечислил
    # кандидатов. «Голѣва → сц Карпово» — не свидетельство, а вопрос.
    # Считать такое доказательством нельзя, и в карточке надо сказать
    # об этом прямо, а не выдавать чужое имя за источник.
    # ------------------------------------------------------------------
    def _key(s):
        t = str(s or '').lower().replace('ѣ', 'е').replace('і', 'и') \
            .replace('ѳ', 'ф').replace('ё', 'е')
        t = re.sub(r'[ъь]', '', t)
        t = re.sub(r'[^а-я]', '', t)
        return re.sub(r'(.)\1+', r'\1', t)

    def _dist(a, b, mx):
        if abs(len(a) - len(b)) > mx:
            return mx + 1
        prev = list(range(len(b) + 1))
        for i, ca in enumerate(a, 1):
            cur = [i]
            for jj, cb in enumerate(b, 1):
                cur.append(min(prev[jj] + 1, cur[jj - 1] + 1,
                               prev[jj - 1] + (ca != cb)))
            if min(cur) > mx:
                return mx + 1
            prev = cur
        return prev[-1]

    def _near(a, b):
        x, y = _key(a), _key(b)
        if not x or not y:
            return False
        if x == y:
            return True
        if len(x) >= 5 and len(y) >= 5 and (x.startswith(y) or y.startswith(x)):
            return True
        n = min(len(x), len(y))
        mx = 0 if n <= 4 else 1 if n <= 7 else 2
        return mx > 0 and _dist(x, y, mx) <= mx

    p50 = r.get('pk1650') or {}
    if p50.get('zh'):
        _z = re.sub(r'▪.*', '', str(p50.get('zapis') or '')).strip()
        # сокращение отрезаем только когда за ним точка или пробел,
        # иначе «Дулово» превращается в «улово», а «Селище» в «елище»
        _z = re.sub(r'^(сц|сел|с|дер|д|слоб|пог|б\.п)[.\s]+', '', _z, flags=re.I).strip()
        if not _near(r.get('name'), _z):
            f['p50n'] = 0                    # имя не сходится
            if p50.get('vopros'):
                f['p50x'] = _z or 1          # и привязка предположительна
        else:
            f['p50n'] = 1
    pk50 = str(p50.get('status') or '')
    zh50 = bool(p50.get('zh'))
    if zh50:
        f['p50'] = 1
        if 'предположительн' in pk50.lower():
            f['p50q'] = 1

    if byla.search(e1):
        f['gn'] = 'was'          # так записано в самой писцовой книге
    elif ((zhil.search(e1) and not pust.search(e1)) or zh50) \
            and pust.search(later + ' ' + kind):
        f['gn'] = 'gone'         # в ПК селение, в поздних источниках пустошь
    elif byla.search(e2) or byla.search(kind):
        f['gn'] = 'ep'           # «что была деревня» знает только межевание

    # ------------------------------------------------------------------
    # Следы человека. Церковь, городище, селище, мельница, пруд, сад,
    # копань, гумно — всё это не возникает на пустом месте: их выкопали,
    # построили, посадили. Значит селение здесь было, даже если ни один
    # источник прямо этого не сказал. Это не примета и не вероятность,
    # а самостоятельное свидетельство, и оно должно вести к выделению.
    # ------------------------------------------------------------------
    # Ищем по ГРАНИЦАМ СЛОВ, а не по кускам: «рига» пряталась внутри
    # «бригадира», «овин» — внутри «Головиной» и «половина», «сад» —
    # внутри «садами». Чин владельца превращался в хозяйственную
    # постройку, фамилия — в овин. Поэтому у каждого признака своя
    # приставка \b и перечень допустимых окончаний.
    # «Церковная земля» в следы НЕ входит: это владение причта, надел,
    # а не храм на этом месте. Церковь считается только тогда, когда
    # названа сама церковь, храм или погост.
    B = r'(?<![А-Яа-яЁёѣіѳ])'          # начало слова с русскими буквами
    SLED = [
        ('церковь', B + r'(церковь|церкви|церковию|храм[ауео]?|погост\w{0,3})'
                        r'(?![А-Яа-яЁёѣіѳ])'),
        ('городище', B + r'городищ\w{0,2}'),
        ('селище', B + r'селищ\w{0,2}'),
        ('мельница', B + r'мельниц\w{0,2}|' + B + r'мельничн\w{0,3}'),
        ('пруд', B + r'пруд\w{0,3}(?![А-Яа-яЁёѣіѳ])'),
        ('сад', B + r'сад(|у|ы|ом|омъ|ами|ах|ахъ|овый|овая)(?![А-Яа-яЁёѣіѳ])'),
        ('копань или колодезь', B + r'(копан[ьи]|колодез\w{0,3}|кладез\w{0,3})'
                                    r'(?![А-Яа-яЁёѣіѳ])'),
        ('кладбище или часовня', B + r'(кладбищ\w{0,2}|часовн\w{0,2})'),
        ('гумно, овин или рига', B + r'(гумн[оаы]|гумнищ\w{0,2}|овин[ыаоъ]?|риг[аиуе])'
                                     r'(?![А-Яа-яЁёѣіѳ])'),
        ('дворище', B + r'дворищ\w{0,2}'),
        ('погреб', B + r'погреб\w{0,3}(?![А-Яа-яЁёѣіѳ])'),
        ('кузница', B + r'кузниц\w{0,2}'),
    ]
    # Ищем ТОЛЬКО в графах, описывающих саму дачу. Служебные — «Событие»
    # и «Примечание» — исключены нарочно: там лежат рабочие заметки,
    # разборы однофамильцев и ссылки на дачи других уездов, и слово
    # «церковь» оттуда относится к чужому месту, а не к этому.
    # И запоминаем, ГДЕ найден каждый признак: без этого проверить
    # находку нельзя, а непроверяемое свидетельство не свидетельство.
    # Признак должен стоять на самой даче, а не при её владельце.
    # «Поповка, церкви Іоанна Златоустаго что въ Бакинѣ» — пустошь во
    # владении церкви, которая стоит в Бакине, в другом селе. «Писцовая
    # церковная земля с. Бакина, церкви Іоанна Златоустаго» — то же.
    # Поэтому из текста вырезаем куски, где слово идёт в родительном
    # падеже как указание владельца: «церкви N», «села N», «деревни N».
    _own = re.compile(
        r'(?:,|\s|^)\s*(церкви|церковнослужител\w+|священник\w*|свящ\.|причт\w*|'
        r'села|сельца|деревни|деревень|слободы|слободки|города|гор\.|погоста|'
        r'монастыря|пустоши)\b[^,.;▪]{0,60}', re.I)

    def _clean(v):
        """Убрать из текста указания владельца, оставив описание места."""
        return _own.sub(' ', str(v or ''))

    _src = [('название', r.get('name')),
            ('вид', r.get('kind')),
            ('дословная запись', r.get('doslovno')),
            ('ЭП: звание дачи', (r.get('ep') or {}).get('zvanie')),
            ('ЭП: примечание', (r.get('ep') or {}).get('primechanie')),
            ('угодья', r.get('ugodya')),
            ('ЭП Белёва: описание', ((r.get('belev') or {}).get('opis'))),
            ('алфавит: звание дачи', ((r.get('galf') or {}).get('zvanie')))]
    _src += [('эпоха ' + k, v) for k, v in (ep or {}).items()]
    _sled, _sledw = [], {}
    for n, rx in SLED:
        where = [nm for nm, v in _src if v and re.search(rx, _clean(v), re.I)]
        if where:
            _sled.append(n)
            _sledw[n] = where[0] if len(where) == 1 else where[0] + ' и ещё'
    if _sled:
        f['sled'] = _sled
        f['sledw'] = _sledw

    # ------------------------------------------------------------------
    # Слова в самом названии, которые говорят о жилом месте. «Нижняя»
    # бывает только там, где есть Верхняя, то есть селение делилось.
    # «Старое» предполагает Новое. Слободка, погостище, селище, городец —
    # прямые названия поселений, а не угодий.
    # ------------------------------------------------------------------
    NSL = [
        ('старое или новое', r'\bстар(ое|ая|ый|о|ые)\b|\bстаро|\bнов(ое|ая|ый|ые)\b|\bновосел'),
        ('верхнее, нижнее или среднее', r'верхн|нижн|средн|вышн'),
        ('большое или малое', r'\bбольш|\bмал(ое|ая|ый|ые)\b'),
        ('погостище', r'погостищ'),
        ('слобода или слободка', r'слобод'),
        ('селище', r'селищ'),
        ('городище, городец или городок', r'городищ|городец|городок|городк'),
        ('починок', r'починок|починк'),
        ('выселки', r'выселк'),
        ('дворище', r'дворищ'),
        ('усадебное место', r'усадебн'),
        ('второе имя через «тожъ»', r'тож'),
    ]
    # Признак должен относиться к САМОМУ месту, а не к его владельцу.
    # «Отхожая пашенная земля бѣломѣстной слободы козаковъ» — это земля
    # при слободе, а не слобода. «Озерки, села Верхнихъ Лучекъ» — озерки
    # при селе, а не «верхнее» селение. Поэтому ищем только в головной
    # части названия: до первого слова в родительном падеже, которым
    # начинается указание владельца, и не ищем вовсе, если название
    # начинается с угодья.
    _nm = str(r.get('name') or '')
    _ug = re.compile(r'^\s*(отхож\w*|земл\w*|пашенн\w*|пашен\w*|пашн\w*|луг\w*|покос\w*|'
                     r'лес\w*|выгон\w*|часть|пол[ъь]?-?пустош\w*|жеребе\w*|усадебн\w*|'
                     r'церковн\w*|писцов\w*|писцев\w*|оброчн\w*|с[ѣе]нн\w*|болот\w*|рощ\w*)',
                     re.I)
    _poss = re.compile(r'[,;]?\s*\b(села|сельца|деревни|деревень|слободы|слободки|города|'
                       r'гор\.|погоста|монастыря|церкви|поселка|селения|дер\.|с\.)\b', re.I)
    if _ug.search(_nm):
        _head = ''                      # угодье: имя места — не о поселении
    else:
        m = _poss.search(_nm)
        _head = _nm[:m.start()] if m else _nm
    _nsl = [n for n, rx in NSL if _head and re.search(rx, _head, re.I)]
    if _nsl:
        f['nsl'] = _nsl

    # Много совладельцев. Довод косвенный: землю дробили по наследству,
    # и дробить было что, если стоял двор. Прямым свидетельством это
    # не является, и в выделении идёт слабее прочих.
    _vl = f.get('vl') or []
    _n_vl = len(_vl)
    if not _n_vl:
        _z = str(((r.get('ep') or {}).get('vladelcy')) or '')
        if _z:
            _n_vl = len([x for x in re.split(r'[,;•▪]', _z) if len(x.strip()) > 4])
    if _n_vl >= 3:
        f['mv'] = _n_vl

    # ------------------------------------------------------------------
    # Судьба селения: интересно не «была пустошью когда-то», а «исчезло
    # и не вернулось». Селение, возродившееся к 1900-м и живое сейчас,
    # выделять незачем — на его месте новая застройка, копать нечего.
    # Разряд тем выше, чем раньше место опустело.
    # ------------------------------------------------------------------
    ORD = [('I', 1628), ('P50', 1650), ('II', 1780), ('VI', 1851),
           ('III', 1857), ('IV', 1900), ('V', 2026)]
    # «в ПК: деревня», «по ПК — село» — слово стоит после двоеточия или
    # тире, а не первым. Прежде такие срезы читались как неясные, и
    # свидетельство писцовой книги пропадало.
    zh_re = re.compile(r'(^|▪|;|:|—|-)\s*(деревня|село|сельцо|починок|слобода|погост)'
                       r'(?![А-Яа-яЁёѣіѳ])', re.I)
    pu_re = re.compile(r'пустош|нежил|урочищ', re.I)

    # Связь с книгой чужого уезда не свидетельствует ни о жизни, ни о
    # запустении: это догадка. Такую клетку считаем неясной.
    UZ0 = {'А': 'Алексин', 'Б': 'Белёв', 'БП': 'Белёв',
           'К': 'Козельск', 'Л': 'Лихвин', 'О': 'Одоев'}
    own0 = UZ0.get(str(rid).split('-')[0], '')
    alien = any(own0 and b and not b.startswith(own0[:5])
                for _, b in re.findall(r'\(([^)]*ПК\s+([А-ЯЁ][а-яё]+)[^)]*)\)', e1))

    # Признаки жизни ищем по всему тексту, а не только в его начале.
    # Прежде срез вида «по СНМ 1859: село Каменка, дворов 17» считался
    # неясным, потому что «село» стояло не первым словом. Из-за этого
    # живые селения попадали в исчезнувшие и получали жёлтое кольцо.
    zhiv_re = re.compile(r'существует|жило[еТ]?\b|жилая|жилое|обитаем|'
                         r'двор(ов|а|ы)?\s*[:\-]?\s*\d|жит\.|м\.\s*п\.|душ\s*\d|'
                         r'населен', re.I)

    # «предположительно = деревня Айдарово… совпадение по основе имени,
    # требует подтверждения» — это догадка составителя, а не сведение
    # о том, что место живо. Прежде такой срез объявлял селение живым
    # и отменял прямое свидетельство писцовой книги о запустении.
    dogadka = re.compile(r'предположительн|требует подтвержд|совпадение по основе|'
                         r'вероятно\s*=|возможно\s*=|\(\?\)', re.I)

    def cell(k):
        if k == 'I' and alien:
            return '?'
        if k == 'P50':
            if not p50.get('zh'):
                return None
            return '?' if p50.get('vopros') else 'да'
        v = str(ep.get(k) or '')
        if not v.strip():
            return None
        # «что была деревня» — свидетельство запустения, идёт первым
        if byla.search(v):
            return 'нет'
        # прямые признаки жизни сильнее слова «пустошь» в том же тексте:
        # «село Каменка, дворов 17» — это жилое, чем бы ни звалась дача
        if zh_re.search(v) or zhiv_re.search(v):
            # но если тут же сказано «предположительно» или «требует
            # подтверждения» — это не свидетельство, а версия
            return '?' if dogadka.search(v) else 'да'
        if pu_re.search(v):
            return 'нет'
        return '?'

    row = [(y, cell(k)) for k, y in ORD]
    if row[-1][1] == 'да':
        f['fate'] = 'alive'                     # живо сейчас — не выделяем
    else:
        alive_i = [n for n, (y, s) in enumerate(row) if s == 'да']
        dead_i = [n for n, (y, s) in enumerate(row) if s == 'нет']
        la = max(alive_i) if alive_i else -1
        after = [n for n in dead_i if n > la]
        # «Пустошь, что была деревня» может стоять не в эпохах, а в виде
        # записи или дословном тексте — эпохи при этом пусты. Свидетельство
        # от этого не перестаёт быть свидетельством: место названо пустошью,
        # и названо бывшим селением.
        byla_all = byla.search(' '.join(str(x or '') for x in [
            kind, r.get('doslovno'), (r.get('ep') or {}).get('zvanie'),
            (r.get('galf') or {}).get('zvanie')]))
        if not dead_i and byla_all and not alive_i:
            f['fate'] = 'gone'
            f['dy'] = 1780                      # известно по межеванию
            f['tfr'] = 2
            f['tfe'] = 1
        elif not dead_i:
            f['fate'] = 'unknown'               # свидетельства «пустошь» нет
        elif not after:
            f['fate'] = 'revived'               # после запустения снова жило
        else:
            y = row[after[0]][0]
            f['fate'] = 'gone'
            f['dy'] = y                         # год, к которому уже пусто
            if alive_i:
                f['hz'] = 1                     # жилым отмечено хоть раз
                f['ly'] = row[la][0]            # последний год, когда жило
            unsure = bool(re.search(r'слито|сверить', e1, re.I)) or alien
            # Разряд считаем не по календарю, а по тому, ЧТО было последним
            # свидетельством жизни. Главные две группы:
            #   1 — в писцовой книге записано «уже пустошь»;
            #   2 — в писцовой книге (1620–30-х или 1650) селение живое,
            #       а в межевании и позже уже пустошь.
            # Год смерти тут вторичен: важно, что место опустело между
            # писцовой книгой и межеванием, то есть в XVII–XVIII веке.
            # Предположительная привязка к книге 1650 г. — всё-таки зацепка,
            # и притом сильная: место названо жилым, а в межевании уже пусто.
            # Исключать такие из главной группы нельзя, но и выдавать за
            # доказанное тоже: помечаем разряд как требующий выбора.
            in_pk = la in (0, 1)                # последняя жизнь — по ПК
            # Лист писцовой книги указан, а вид записи не выписан. Что там
            # стояло — деревня или уже пустошь — неизвестно, но известно ГДЕ
            # смотреть. Это не пробел, а невыполненная работа: помечаем как
            # зацепку и просим выписать.
            listy = (r.get('pk') or {}).get('listy')
            if not in_pk and listy and not e1.strip():
                in_pk = True
                f['tq'] = 1
                f['nv'] = 1                     # вид записи не выписан
            if not in_pk and p50.get('zh') and p50.get('vopros') and not f.get('p50x'):
                in_pk = True
                f['tq'] = 1                     # разряд держится на догадке
            # «Пустошь, что была деревня» — прямое свидетельство запустения.
            # Помета «слито — сверить» ставит под сомнение ПРИВЯЗКУ записи
            # к листу книги, но не сам факт: составитель прочёл эти слова
            # в источнике. Поэтому находкой считаем, а неуверенность
            # показываем пунктирным кольцом.
            if byla.search(e1) and not alive_i:
                f['tfr'] = 1
                if unsure:
                    f['tq'] = 1          # свидетельство есть, привязка спорна
            # то же сказано в межевании: вид записи, звание дачи, дословно
            elif byla.search(kind + ' ' + str(r.get('doslovno') or '')
                             + ' ' + str((r.get('ep') or {}).get('zvanie') or '')) \
                    and not alive_i:
                f['tfr'] = 2
                f['tfe'] = 1             # известно только по межеванию
            elif in_pk:
                f['tfr'] = 2
            elif _sled and not _ug.search(_nm):
                # следы человека на месте — самостоятельное свидетельство,
                # но не тогда, когда дача сама по себе угодье: пруд при
                # отхожей земле принадлежит соседнему селению, а не ей
                f['tfr'] = 2
                f['tfs'] = 1              # разряд держится на следах
            elif _nsl:
                # само название говорит о поселении
                f['tfr'] = 2
                f['tfn'] = 1
            # Одно только число совладельцев разряда НЕ даёт. Довод слишком
            # косвенный: землю дробили и под угодьями. «Отхожая пашенная
            # земля полъ-сельца Ровнаго» четырёх владельцев имеет, а
            # селением не была никогда. Признак остаётся в карточке
            # подсказкой, но кольца не даёт.
            elif y <= 1780:
                f['tfr'] = 3
            elif y <= 1857:
                f['tfr'] = 4
            elif y <= 1900:
                f['tfr'] = 5
            else:
                f['tfr'] = 6

    # ------------------------------------------------------------------
    # Несверенные сведения. Помета «слито — сверить» означает, что запись
    # свели с чужой книгой предположительно. Строить на таком высший разряд
    # находки нельзя: это не свидетельство, а рабочая догадка.
    # Отдельно ловим случай, когда книга чужого уезда: писцовая книга
    # Алексина ничего не может сказать о лихвинской даче, кроме как
    # по пограничным дачам, а их разбирают поштучно.
    # ------------------------------------------------------------------
    if re.search(r'слито|сверить', e1, re.I):
        f['sv'] = 1
    UZ = {'А': 'Алексин', 'Б': 'Белёв', 'БП': 'Белёв', 'К': 'Козельск',
          'Л': 'Лихвин', 'О': 'Одоев'}
    own = UZ.get(str(rid).split('-')[0], '')
    for m in re.findall(r'\(([^)]*ПК\s+([А-ЯЁ][а-яё]+)[^)]*)\)', e1):
        if own and m[1] and not m[1].startswith(own[:5]):
            f['pkm'] = m[1]          # книга чужого уезда
            break

    # ------------------------------------------------------------------
    # Дача из нескольких селений. В графе названия стоит одно, головное,
    # а Экономические примечания перечисляют все: «Село Хлыстово и деревни
    # Карачева и Туромшина». Раскладывать это на отдельные имена машиной
    # ненадёжно — владельцы и угодья путаются с селениями. Поэтому берём
    # перечень ДОСЛОВНО, как он записан в источнике, и только отсекаем
    # владельцев: они идут после запятой с именем или чином.
    # ------------------------------------------------------------------
    zv = str(((r.get('ep') or {}).get('zvanie')) or '')
    if zv:
        head = re.split(r',\s*(?=(?:граф|княз|кн\.|[А-ЯЁ][а-яё]+[ая]\s+[А-ЯЁ]))',
                        zv)[0]
        head = re.sub(r'\s*,?\s*(и\s+)?с[ъь]?\s+выд[ѣе]ленною.*$', '', head, flags=re.I)
        head = re.sub(r'[.,;\s]+$', '', head).strip()
        # несколько селений видно по связке «и деревни», «съ деревнею» и т. п.
        if re.search(r'\s(и|съ|с)\s+(деревн|сельц|сел[оа]|полудеревн|полсельц|'
                     r'починк|погост|слобод)', head, re.I) and 3 < len(head) < 90:
            f['mn'] = head

    # какие писцовые книги названы: их несколько, и это разные источники
    books = []
    if zh50:
        books.append('ПК 1650')
    for m in re.findall(r'\(([^)]*ПК[^)]*)\)', e1):
        b = re.sub(r',.*', '', m).strip()
        if b and b not in books:
            books.append(b)
    if books:
        f['pkb'] = books
    # где именно позднее названо пустошью — пригодится в подписи
    if f.get('gn') == 'gone':
        where = [n for n, k in (('межевание', 'II'), ('Кёппен', 'III'),
                                ('трёхвёрстка', 'IV'), ('списки', 'V'))
                 if pust.search(str(ep.get(k) or ''))]
        if where:
            f['gnw'] = where

    oc = ((r.get('pustosh') or {}).get('ocenka') or '').strip()
    if oc:
        f['oc'] = oc[:1]
    if r.get('stan'):
        f['st'] = r['stan']
    return f


def main(src_dir, out_path):
    facets, allrecs, n = {}, {}, 0
    for fn in sorted(os.listdir(src_dir)):
        if not fn.endswith('.json'):
            continue
        with open(os.path.join(src_dir, fn), encoding='utf-8') as fh:
            data = json.load(fh)
        if not isinstance(data, dict):
            continue
        for rid, r in data.items():
            if not isinstance(r, dict):
                continue
            facets[rid] = facet(r, rid)
            allrecs[rid] = r
            n += 1
    # Одна дача на плане — несколько записей в реестре: в алфавите под
    # одним номером перечислено несколько урочищ. Ставить метку по номеру
    # приходится вслепую, поэтому помечаем такие записи и называем соседей.
    same = {}
    for rid in facets:
        rec = allrecs.get(rid) or {}
        nn = rec.get('n_plan')
        if nn:
            try:
                same.setdefault((str(rid).split('-')[0], int(float(nn))), []).append(rid)
            except (TypeError, ValueError):
                pass
    for ids in same.values():
        if len(ids) > 1:
            for rid in ids:
                facets[rid]['dn'] = len(ids)
                facets[rid]['dsame'] = [x for x in ids if x != rid][:8]
    with open(out_path, 'w', encoding='utf-8') as fh:
        json.dump(facets, fh, ensure_ascii=False, separators=(',', ':'))
    print('записей: %d, файл: %.2f МБ' % (n, os.path.getsize(out_path) / 1048576))


if __name__ == '__main__':
    main(sys.argv[1] if len(sys.argv) > 1 else 'reg',
         sys.argv[2] if len(sys.argv) > 2 else 'facets.json')
