Zum Inhalt

Пошук: канали, склейка, поріг

Пошук у прочитаному — не grep. Машинний текст скоропису XVIII–XIX ст. має частку помилок у десятки відсотків, і потрібне прізвище виходить із рушія спотвореним до невпізнання. Тому шукається схожість нормалізованих токенів, а не підрядок.

Це документ про пошук у вже прочитаному. Де взяти матеріал, якого в просторі ще немає, у які джерела йти і в якому порядку — where-to-dig.md.


Три сховища, а не три «канали»

search.run має поле where, і це вибір сховища, а не способу пошуку:

where що шукає коли брати
decode (дефолт) машинний текст усіх прогонів шукаємо там, де ще ніхто не дивився
pages прізвища, виписані оком у облік перевірити, чи не знаходили це раніше
records учасники розібраних записів (батьки, восприємники, свідки) шукаємо роль, а не згадку

Роль і тип акту — поля, а не читання очима. У where=records є role (father, godfather, deceased…) і rtype (birth, death…): «хто був батьком із цим прізвищем» і «хто був восприємником» — різні питання, і саме заради них записи розбирають структурою. Фільтр, який не діє в обраному сховищі, відмовляється, а не мовчить: тихо ширша вибірка виглядає як звужена, і нуль по ній закриває напрям, якого не перевіряли.

Вісь: прізвище чи місце. axis=place (у pages і records) шукає не по прізвищу, а по поселенню — «які акти згадують це село, байдуже під яким прізвищем». Місце тримається двічі: на записі (де відбулась подія) і на учаснику (звідки він), і в шлюбі це різні села.

nysh records grep "Ковальський" --role father --json
nysh records grep "Городківка" --axis place --json

Шифра — це запит іншого роду

catalog.search (nysh find) розрізняє два питання. «Мястківка» — це «де взагалі є щось про моє село», і відповідь на нього перелік. «127-1078-1662» — це адреса конкретної справи, і відповідь одна.

Доти на адресу відповідав текстовий пошук, тобто не відповідав ніколи: три числа поспіль не трапляються в жодному заголовку. Нуль давала й «ДАВіО-172-4-112» — рядок, який пошук САМ друкує в кожному хіті як адресу справи, тобто показане не можна було набрати назад.

Адресний маршрут питає інші місця й у такому порядку: бібліотека (справа може вже лежати на цій машині) → реєстр опису (заголовок, роки, аркуші, звідки взяти) → каталоги, які вміють шукати за шифрою.

🔴 Це видно у відповіді, і мусить бути видно: coverage.searched містить рівно те, що питали, а попередження address_route каже прямо, що повнотекстового пошуку по каталогах НЕ було. Джерело, яке вміє лише текстом, іде в unavailable з причиною — його нуль не є нулем про цю справу.

Якщо адреса не знайшлась ніде, запит іде текстом як звичайний: рядок може бути лише схожий на шифру (дата «1858-03-14»). Примусово текстовий маршрут — nysh find … --text.

🔴 Нуль у кожному з трьох означає різне. «Немає в decode» — немає в машинному тексті, який міг це слово скалічити. «Немає в pages» — ніхто не виписував, і це майже нічого не каже про джерело. «Немає в records» — ніхто не розбирав записи, навіть якщо прізвище там є.

Тому знаменник у відповіді різний за змістом, і в звіті його треба називати разом зі сховищем, а не окремо.


Де лежить текст: стор

Пошук по декоду читає не теки прогонів, а текстовий стор — один файл SQLite (data/derived/text_store.sqlite) із сирим текстом сторінок, рядками й рамками, тригамним індексом і готовими кандидатами: склейки через рядок і за колонкою вже в них. Обхід тек прогонів на великому корпусі коштує хвилини, стор відповідає за секунди. search.run бере стор сам, щойно той покриває область, і каже про це в coverage.backend; прогони поза стором ідуть старим шляхом.

nysh text state            # скільки прогонів у сторі, скільки застаріло
nysh text index            # догнати після нових прогонів
nysh text state --verify   # довести однорідність після обірваної перебудови

Стор, який саме пише інша сесія (повна індексація, optimize, наглядач хмари після справи), text index чекає до 30 хв, а не пропускає: поступ каже «чекаю стор: пише інша сесія». Прогін, який за цей час проіндексував хтось інший, удруге не збирається. Лише понад 30 хв прогін лишається поза стором — і це тривога в locked_skipped, а не примітка.

Де файл і скільки процесів. Стор росте до десятків ГБ, а корпусний пошук читає його майже цілим. На повільному диску його виносять полем маркера простору — text_store = "<тека>" у [workspace] файла nyshporka.toml (nysh text state друкує, звідки читає). Блоки корпусу чешуть три процеси разом, кожен бере до 1.5 ГБ пам'яті; на слабкій машині — NYSH_SWEEP_WORKERS=1.

🔴 Відбиток правил склейки, який друкує state, — це заява, а не доказ. Якщо перебудову обірвано (брак пам'яті, Ctrl+C, закрите вікно), приймач — --verify: він перераховує кандидатів звірених сторінок чинним кодом і називає прогони, де вони розійшлись. Код виходу 1 означає, що пошук по цих прогонах відповідає не тим, чим по решті.

⚡ Швидкий індекс для великих областей

Свіп триграмами по всьому корпусу — хвилини на прізвище (на 3 млн сторінок: 4–17 хв трьома воркерами). Необов'язковий індекс Tantivy поруч зі стором знаходить рядки-кандидати за десятки секунд, а бал і далі ставить той самий rapidfuzz із тим самим порогом.

nysh text index --fast     # зібрати (години на великому сторі) або догнати
  • Ставиться окремо: pip install nyshporka[fast]. Без пакета чи без індексу все працює свіпом, як раніше.
  • Береться сам, коли область від ~200 тис. сторінок (корпус, великий фонд). Справа й невелика серія лишаються на свіпі — там він дешевший.
  • 🔴 Нуль по області підтверджує свіп. Передфільтр губить частину смуги 90–94; «немає» без свіпу триграмами не друкується.
  • Прогін, змінений після збірки індексу (новий штамп чи правила склейки), іде свіпом, доки nysh text index не дожене індекс. Зіпсований індекс — теж свіп, і відповідь каже про це полем fast_error.
  • Заміряно на корпусі 3 млн сторінок: прізвище з 22 формами — 2,4 хв проти ~17, поширене коротке — 1,7 хв проти 4,2; на голдсеті роду знайдено ті самі справжні згадки, а сторінок на око — у 2,4 раза менше.

👥 Два слова поруч: nysh text near

Часто шукають не одне слово, а пару: ім'я з прізвищем, прізвище з селом, ім'я з по батькові, два прізвища одного запису, стан з іменем («однодворецъ» + «Іосифъ»). Окремий запит кожним словом дає дві купи збігів, і звести їх доводиться оком.

nysh text near "Ковальський" "Іван" --case <справа>         # той самий рядок або сусідній
nysh text near "Ковальський" "Городківка" --window 3        # по всьому прочитаному
  • Довше слово шукається по всій області тим самим свіпом, що й прізвище (на корпусі — швидким індексом), коротше — лише на рядках ±--window біля нього. Порядок слів у команді не важить.
  • Обидва слова розгортаються, як у пошуку прізвища: гніздо імен, написання роду з профілю. Розгорнутий двійник коротший за 4 літери («jan» від «Іван») у пару не йде: він сидить усередині будь-якого слова («Стефанъ»). Набране людиною лишається.
  • Кандидат, який схожий на перше слово не менше, ніж на друге, другим не рахується: «Іван» усередині «Іваненко» на тому самому місці — не пара.
  • --window 0 — той самий рядок, 1 (типово) — ще й сусідні: перенос і колонки. Слово в кінці рядка стор склеює з початком наступного, тож для нього вікно вниз на рядок ширше.
  • Нуль — зі знаменником: на скількох рядках і сторінках знайдено перше слово. «Пари немає» при сотні місць першого і при нулі — різні відповіді.

⚠ Межа вікна — рядки, а не запис. Ознаки родини з різних колонок одного запису зводить канал запису (text find --case), і лише для людей роду з профілю.

Регекс по сирому тексту замість rg — nysh text grep; усі канали пошуку роду разом, із журналом — nysh text find (див. surface.md).


Що робить пошук по декоду сам

Нормалізація обох сторін

Запит і текст зводяться до спільної форми: кирилиця й латинка, історичні літери, типові плутанини самого рушія. Тому запит кирилицею знаходить латинський запис і навпаки — а точний підрядок не знайшов би нічого.

🔑 Гніздо написань імені

Нормалізація вирівнює письмо, але не знає, що Явдоха — це Євдокія. Тому запит іменем додатково розкривається довідником церковних імен: шукається все гніздо, а не лише набране слово.

Чому це не косметика — заміряно на живому rapidfuzz при порозі 80:

набрали у книзі стоїть бал знайшлось би без довідника
Явдоха Євдокія 61.5 ні
Осип Іосиф 66.7 ні
Оксана Ксенія 66.7 ні
Семен Симеон 72.7 ні
Ганна Анна 88.9 так

Тобто для більшої частини гнізда це різниця між знахідкою й мовчазним нулем, а не «трохи нижче порога».

⚠ Прізвища так НЕ зводяться. «Гончаръ» і «Ганчаръ» можуть бути різними родами; їх зближує тільки fuzzy, і рішення лишається за оком.

Вимикач — --no-given. Він потрібен рівно для одного: порівняти, що додав довідник, коли хітів забагато.

🔑 Написання прізвища з профілю

Профіль простору знає прізвище десятком написань (відмінки × орфографії), і пошук бере їх усі, коли запит справді про це прізвище. Одне число знаменника замість десятка окремих нулів.

Що це дає, заміряно при порозі 80 на профілі «Сікорський»:

у книзі стоїть лише набране + написання профілю
корскаго (з'їдено початок) 58.8 85.7 ← інакше не знайдеться ніколи
Сикорскаго 87.5 100.0
Сикорского 87.5 100.0

Рушій з'їдає початок слова частіше, ніж кінець, і саме такий уламок лишається поза порогом, поки шукають однією формою.

⚠ Чужий запит форми роду за собою не тягне: пошук по назві села лишається пошуком по назві села. У відповіді видно, чиї написання підмішано.

🏠 Рід — будь-який профіль файла, а не лише активний. Файл config/research_profile.yaml тримає кілька родів (profiles), а fallback лише каже, який із них — рід самого простору. Запит шукає свій рід серед УСІХ: «Коваленко» дістає написання профілю Коваленків, навіть коли простір веде інший рід. Назвати рід явно — --family <ключ профілю>. Від роду беруться написання, правила рангу (confusers, rank_down), люди-якорі й ознаки запису.

Друга родина заводиться тією самою командою, що й перша, і fallback не чіпає: nysh profile init <Прізвище> --name <ключ> --paradigm <парадигма>.

Вимкнути — --no-profile.

🔑 Ранг: чуже слово опускає хіт, але не забирає

Профіль тримає confusers (сусідні прізвища на той самий хвіст) і rank_down (іменовані регекси службових формул). Хіт, який чуже слово пояснює краще, ніж шукане прізвище, падає вниз видачі з названою причиною — і лишається у видачі.

🔴 Це не фільтр, і різниця тут не косметична. Ціна помилки асиметрична: хибний плюс людина відсіює за секунди, а пропущений аркуш не відсівається нічим, бо його ніколи не буде в списку.

Навіщо взагалі: бал сам по собі не відділяє рід від рутини. Замір приватного конвеєра — 60 сильних кандидатів, з них рубрика книги 26, сусідній рід 19, шуканий рід нуль, і всі три верхні місця за балом займала рубрика.

Вид збігу й бал слова

Кожен хіт несе match — як стем ліг на слово, — а відповідь несе розклад matches за цими видами:

match що це бал
exact слово після нормалізації — саме стем 100
variant схоже слово тієї ж міри (рушій перекрутив літери) схожість цілого слова
ending стем — початок довшого слова: інший відмінок чи закінчення (латинський «Ja-» на місці «Я» — теж) бал вікна, не вище 99
inside стем усередині довшого слова: злиплі рушієм ім'я й прізвище або чуже прізвище з тим самим хвостом бал вікна, не вище 99

Порядок видачі: клас рангу (вище) → вид збігу (exact, далі variant і ending разом за балом, далі inside) → бал. 🔴 inside — не відсів і не «не наш»: злипле «Олександрярошинський» — теж рід. Він лише нижче. Замір 07.10.2026 на «Ярошинський»: із 296 тис. збігів inside — 240 тис., точних 24; доти бал рахувався по найкращому вікну слова, і «Pan Paroszynskiin» мав ту саму сотню, що й «Ярошинський». Порівнюючи бали з давніми записами, пам'ятай, що стара сотня могла бути inside.

⚠ rank_down — регекси, і порівнюються вони з нормалізованим текстом (щ→sc, ч→c, и/і→i, кирилиця в латинку). Правило, написане кирилицею, не спрацює ніколи; пошук каже про це вголос, а не мовчить.

⚓ Канал імен: коли прізвища в тексті вже немає

--anchors шукає рядки, де ім'я й по батькові роду стоять поруч. Це інший вхід, а не ширший поріг: рушій калічить довге прізвище сильніше, ніж коротке формулярне слово, тож у приватному конвеєрі обидва золоті рятунки мали по прізвищу бал нуль, а по батькові той самий рушій прочитав дослівно.

Заміряний виграш там: прізвищний канал 61-72%, разом із якорями 85-91% (знаменники різні — 97, 75 і 73 аркуші, і число без знаменника не переносьте).

Три правила, кожне куплене заміром:

  • пара обов'язкова. Одиночне «Іосифовъ» стоїть у метриці на кожному аркуші; канал, який приймає одинака, знаходить усе, тобто нічого;
  • вікно років, і воно РІЗНЕ. Ім'я корисне, поки людина жива; по батькові — поки живі її діти (нар+18 … нар+100), тобто воно переживає носія на покоління. Без прив'язки до років якір вироджувався до чверті рядків книги;
  • без дати особа якір не звужує і за замовчуванням у нього не йде — але про це сказано числом, а не мовчки.

Якорі беруться з поля kin профілю роду, а канон осіб їх доповнює (canon.md) — лише для роду самого простору (fallback): люди канону іншій родині не родичі, і їхні імена в її якорях знаходили б наш рід під виглядом чужого. Порожній канон звужує канал до kin. Канал без kin і без канону мовчить — це не нуль.

Формат kin — перелік людей роду під профілем:

profiles:
  kovalenko:
    surname: {display: Коваленко, paradigm: noun_ko, stems: {uk: Коваленк, ru_prereform: Коваленк}}
    kin:
      - {given: Тимофій, patronymic: Митрофанов, born: 1847, died: 1923}
      - {given: Фекла, patronymic: Харитонова, born: 1851}
      - {given: Ганна, patronymic: Степанова, surname: Мельник, born: 1872}

born/died — роки; без жодного людина в якорі не йде (вікно не звужує). surname — прізвище людини, коли воно не прізвище роду (дівоче в дружини): каналу запису це ще одна ознака. Рік пишіть із запису, звіреного оком, а не з пам'яті родини: вікно, зсунуте на покоління, мовчки вимикає людину.

⚠ Працює в межах справи: вікно береться з її років. Поза справою операція відмовляється, а не шукає наосліп.

🏠 Канал запису: родина з різних колонок одного запису

--record (у text find — завжди в межах справи) зводить ознаки родини — прізвище роду, ім'я, по батькові, прізвище людини з kin — у межах одного запису, а не рядка. Метрика, сповідка, ревізія — таблиці: ім'я дитини в одній колонці, батьки в другій, кума в третій, і рушій читає їх окремими рядками. Прізвищний канал і канал імен дивляться на рядок, тож запис, де прізвище батька скалічене, для них не існує.

Заміряно на метриках одного села й парафій навколо (47 книг, родина з 13 людей kin, 17 записів, звірених оком):

прізвищний канал канал запису
народження 1870-х, де батька (ім'я, по батькові, прізвище) рушій скалічив до невпізнання серед 4475 збігів книги — немає 1-ше місце зі 131 запису книги
знайдено звірених записів — 16 із 17: 6 — першими у своїй книзі, 12 — у першій шістці

Що втрачено, названо: запис польською 1923 р., де ім'я розірване переносом («Pro- / kopij»), а латинський голос не має геометрії — дві ознаки на сторінці без меж запису кандидатом не стають (нижче).

Правила, кожне куплене заміром:

  • межа — запис, а не сторінка. Запис — смуга шести медіанних рядків у геометрії сторінки, через усі колонки й обидві половини розвороту. На сторінці чотири-вісім записів, і зведене по сторінці з'єднує чужих людей: ім'я сина з одного запису, по батькові батька з другого, ім'я матері з третього;
  • вага ознаки — неймовірність випадкового збігу в самій смузі. Частота ознаки f — з книги, на слово; смуга з N слів зустріне її випадково з імовірністю 1 − (1 − f)^N, вага — −ln цієї імовірності. Шапка графи й «Марія» в довгій смузі важать майже нуль самі собою, та сама «Марія» в короткому записі метрики — ні; масове в селі прізвище роду просто важить менше. Ручних ваг і стоп-списків немає. Ознаки, які смуга звичайного розміру зустрічає частіше, ніж у половині випадків, відповідь називає;
  • одне слово — одна ознака. «Митрофаъ» схожий і на ім'я Митрофан, і на по батькові Митрофанов; зарахований двічі, він робив «родину» з одного спотвореного слова;
  • по батькові — лише слово у формі по батькові. «Іоаннъ» і є Іван, але це ім'я сусіда, а не «Ивановъ» нашої людини; польське «syn Mitrofana» — форма по батькові;
  • голос без геометрії зводиться по сторінці, з позначкою, і там потрібна на одну ознаку більша.

🛑 Межа: щільний поіменний реєстр. Родовідна книга, список, сповідка з сотнями імен на аркуші — не жанр каналу. Там імена стоять гронами, і будь-яка смуга ловить кілька частих імен родини випадково: на ДАХмО 230-1-13 (родовідна книга, ~980 тис. слів, 47 людей канону) контрольний акт роду стоїть 420-м із 3470. Для списку працює блок за абеткою, а не канал запису; число в журналі тут не відповідь «роду немає».

Канал подає запис на око, а не вирішує: двоє людей з однаковими ім'ям і прізвищем в одному селі — звичайна річ, і розрізняє їх лише повний запис. Журнал find міряє канал тим самим знаменником ока, що й самоперевірку («око 2/2»).

🎯 Самоперевірка: чи бачить пошук те, що око вже знайшло

--selfcheck міряє recall на аркушах, де прізвище виписала людина, дивлячись на скан. Це третє з трьох чисел, без яких нуль не є відповіддю.

  • знаменник — позитиви ∩ фактично декодовані сторінки: аркуш, який око знає, а рушій не читав, у знаменник не входить, інакше ми міряли б повноту декоду;
  • два числа, не одне: знайдено і подано на око. Розрив між ними — те місце, де вимірювач починає брехати у свій бік;
  • позитивів немає → відмова, а не нуль. Порожній recall і нульовий recall означають протилежне.

🧾 Чим справу вже шукали

Пошук у межах справи лишає слід: запит, поріг, скільки знайшов, якими моделями. Якщо справу вже прочісували іншим рушієм, наступний пошук каже про це — бо «обшукано» протухає мовчки: та сама справа тим самим запитом дала +8 аркушів роду від самої лише зміни моделі.

🔑 Побутове ім'я — окремий шар, вимкнений

Хрестильне ім'я за святцями й побутове, яким людину звали далі, бувають різними іменами: у метриці Васса, у всіх пізніших документах Анна. Це не варіанти написання — це дві різні святі (бал 44.4), і жодна фонетика їх не зблизить.

Такі пари живуть окремим шаром і вмикаються прапорцем --folk. За замовчуванням шар вимкнений: на чужому матеріалі він злипає двох різних жінок, а хибний позитив коштує дорожче за пропущений — пропущене шукають далі, а зліплене вважають знайденим і закривають напрям.

🔴 Кожен хіт несе stem_origin. Значення folk означає, що зв'язок біографічний, а не орфографічний: така знахідка сама по собі нічого не доводить, доки не показано, що йдеться про ту саму людину.

Свої пари додаються в просторі — config/folk_names.yaml:

Мотрона: [Матрёна, Мотря]

🔑 Склейка розірваного прізвища

Прізвище на межі рядка розривається переносом, і жодна половинка сама по собі не схожа на ціле. Пошук тримає хвости трьох попередніх рядків і пробує зшити їх із початком поточного.

Чому саме трьох, а не одного: у табличних бланках (сповідки, ревізії, формуляри) сегментація зшиває колонки, і половинки розходяться на два-три рядки. З вікном в один рядок такі записи не знаходяться взагалі.

⚠ Далекі склейки суворіші за сусідню. Через рядок-два половинки злітаються вже не за законом переносу, а випадково — і без цієї суворості пошук почав би знаходити прізвище в будь-якому шумі.

Практичний наслідок для тебе: не роби склейку руками. Побачивши в тексті обірваний токен, не дописуй його — це вже зроблено, і зроблено з порогами, які перевірені на контрольних запитах.

🔑 Вікно замість рядка

search.run має поле context (типово 1): кожен хіт приходить із сусідніми рядками, а не голим рядком.

Це не зручність. Рядок сам по собі не розрізняє прізвищ зі спільним коренем, а в одній парафії їх буває кілька — заміряно на метриках одного села: 78 кандидатів верхівки розклались на три різні роди з тим самим коренем плюс причт, і за самим рядком вони зливаються в купу однаково правдоподібних хітів.

Розрізняє їх сусідство:

    ↑ Балтскаго Повѣта Села Ковалевскаго      ← географія стоїть вище
    » Дьячисъ Григорій Ковалевскій сынъ Свя   ← сам хіт обривається
    ↓ щунническій получившей удостость        ← слово продовжується нижче

⚠ Вікно розсувне, а не ±N рядків. Огризок контекстом не вважається: сусідній рядок «на», «и», «3» не пояснює нічого, тож вікно розсувається далі, доки не набереться змістовного. Без цього контекст виявляється порожнім рівно там, де він найпотрібніший — між колонками щільного формуляра.

🔑 Другий голос поруч

Якщо справу читано двома рушіями, до хіта додається читання другого на тому самому рядку:

    » Дьячисъ Григорій Ковалевскій сынъ Свя
      2-й голос: Дьячисъ Григр̆ Ковааевскіи сынъ све

Збіг голосів означає надійне читання; розбіжність — сигнал, а не шум. Рушій із мовною моделлю підставляє правдоподібне слово, а другий судить кадри незалежно й калічить локально, зберігаючи корінь. Розбіжність саме на прізвищі (як вище: «Ковалевскій» проти «Ковааевскіи») означає, що ознака в пікселях — і вирішувати має око, а не третій алгоритм.

🛑 Але два голоси підтверджують читання, а не приналежність. Те, що обидва прочитали прізвище однаково, не робить особу вашою.

Поріг схожості

thresh за замовчуванням 80 зі 100. Нижче — більше шуму, вище — губляться саме ті спотворені форми, по які пошук і кликали.

🔴 Не піднімай поріг, щоб «прибрати шум». Рушій калічить середину слова сильніше за краї, тож найцінніші кандидати мають бал ближче до порогу, ніж очевидні. Замість підняття порогу — ранжуй видачу (workflows.md, крок 3).

⚠ Окремо: короткі токени відсіюються довжиною (одиночний — від чотирьох літер, пара — від шести сумарно). Спроба додатково глушити «рядки-огризки» з волокон паперу перевірялась заміром і дала 0.1% менше кандидатів при нулі втрачених хітів — тобто фільтр не потрібен, а от глушіння такого рядка разом із його хвостом з'їдало справжні склейки.


Чого в пошуку немає

Це чесна межа версії, а не прогалина в документації.

немає що це було б що робити натомість
канал за топонімом у декоді «чи бачить машинний текст село, яке точно там є» — знаменник для нуля звичайний запит назвою села; географію звіряти з каталогом. У виписаному й розібраному топонім має власну вісь — axis=place
канал за складом двору у списку двір сповідки чи ревізії, де родина — дванадцять рядків поспіль канал запису ловить запис метрики (смуга шести рядків); двір довшої форми — читати сторінку цілком навколо знайденого
канал за станом + іменем із вагою «однодворець + Іосиф», зважене частотою слова в книзі nysh text near "однодворецъ" "Іосифъ" — пара у вікні рядків, без ваги: часте слово стану стоїть на кожному аркуші
розрив між колонками окремим каналом коли прізвище розірване не переносом, а версткою склейка в три рядки плюс вікно контексту покривають більшу частину цього; ознаки з різних колонок одного запису зводить канал запису
перевірка віку/дат кандидата «чи міг цей запис бути нашою особою» арифметика документа вручну; цифри з декоду не факт

🛑 Якщо людина просить «прочешіть повіт» або «знайдіть усі згадки роду в фонді» — скажи прямо, що в цій версії є три канали роду (прізвище, імена й запис; два останні — у межах справи), пара слів (text near) і три сховища. Обіцяти багатоканальний прочіс, а потім видати нуль з одного каналу — помилка, яку дорого виправляти: цей нуль виглядатиме як вичерпна відповідь.


Як читати нуль по кожному сховищу

Разом із результатом приходить попередження. Читай його, а не лише число:

  • zero_with_denominator — «не знайшлось у N прогонах (M сторінок)». Це справжній нуль, і в звіті він так і пишеться: з N і M.
  • no_denominator — не шукало ніщо. Звітувати «немає» не можна.
  • попередження про метод — якщо сторінки заносились із позначкою «читав лише декод», їхній вміст успадкував помилки рушія.

І головне, що не приходить попередженням: нуль у decode не покриває сторінок, яких ніхто не читав машиною. Скільки їх — питай cases.list і pages.status, а не пошук.