strom-research 1.18.0-beta.12 → 1.18.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.cs.md +4 -3
- package/CHANGELOG.de.md +6 -3
- package/CHANGELOG.md +4 -2
- package/dist/core/document.js +196 -32
- package/dist/core/tree.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
7
7
|
|
|
8
8
|
## 1.18.0
|
|
9
9
|
|
|
10
|
-
*Připravuje se: bety do 1.18.0-beta.
|
|
10
|
+
*Připravuje se: bety do 1.18.0-beta.14. Oddíl bude úplný s vydáním.*
|
|
11
11
|
|
|
12
12
|
- **Fotky z telefonu nastojato.** Fotka otočená podle údaje EXIF se čte tak, jak se má dívat, nastojato; místa na ní
|
|
13
13
|
vyznačená (výřezy, poloviny dvoustrany, uložená část) jsou místa obrázku, jak se ukazuje. Fotka sama zůstává, jak
|
|
@@ -20,8 +20,9 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
20
20
|
matriky, dopis, listinu, náhrobek, rub fotky: přepis slovo od slova, překlad, význam starých slov a zkratek a osoby,
|
|
21
21
|
které jmenuje, i s tím, kdo ze stromu to může být. Nic se nedomýšlí: jméno, které v dokumentu není, vypadne — i
|
|
22
22
|
příjmení, nikdy se nebere od otce ani z jiné rodiny —, co nejde přečíst, je označené; druhý čtenář může číst naslepo.
|
|
23
|
-
Dokument na úředním formuláři, jako rodný list, si své osoby ponechá — dítě i oba rodiče s jejich podílem
|
|
24
|
-
přečtený dřív je po aktualizaci ukáže, aniž by se
|
|
23
|
+
Dokument na úředním formuláři, jako rodný list, si své osoby ponechá — dítě i oba rodiče s jejich podílem, i jméno a
|
|
24
|
+
příjmení psané proloženě a rodné příjmení po „roz.“ —, a dokument přečtený dřív je po aktualizaci ukáže, aniž by se
|
|
25
|
+
četl znovu. U každé osoby a události je i místo na stránce, kde stojí, takže aplikace Strom ukáže výřez každé z nich.
|
|
25
26
|
Do stromu se z toho nic nezapíše samo. Dokument už přečtený to o sobě řekne, takže se omylem nečte podruhé. Dokument
|
|
26
27
|
čtený znovu se ukazuje pod názvem svého souboru, nikdy pod kódem. Dokument poslaný z aplikace Strom najde agenta
|
|
27
28
|
stejně jako terminál — i nainstalovaného do vlastní složky, přes npm nebo Homebrew — místo hlášení, že agent není
|
package/CHANGELOG.de.md
CHANGED
|
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
7
7
|
|
|
8
8
|
## 1.18.0
|
|
9
9
|
|
|
10
|
-
*In Vorbereitung: die Betas bis 1.18.0-beta.
|
|
10
|
+
*In Vorbereitung: die Betas bis 1.18.0-beta.14. Der Abschnitt ist mit dem Release vollständig.*
|
|
11
11
|
|
|
12
12
|
- **Handyfotos aufrecht.** Ein Foto, das nach seiner EXIF-Ausrichtung gedreht ist, wird gelesen, wie es gesehen
|
|
13
13
|
werden soll, aufrecht; die darauf markierten Stellen (Ausschnitte, die Hälften einer Doppelseite, ein gespeicherter
|
|
@@ -21,8 +21,11 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
21
21
|
Wort, ihre Übersetzung, was alte Wörter und Abkürzungen bedeuten, und die genannten Personen, samt wem im Stammbaum
|
|
22
22
|
sie entsprechen können. Nichts wird ergänzt: ein Name, der nicht im Dokument steht, fällt weg — auch ein Nachname, nie
|
|
23
23
|
vom Vater oder aus einer anderen Familie genommen —, Unleserliches wird markiert; ein zweiter Leser kann blind lesen.
|
|
24
|
-
Ein Dokument auf einem amtlichen Formular, etwa eine Geburtsurkunde, behält seine Personen — das Kind und beide
|
|
25
|
-
|
|
24
|
+
Ein Dokument auf einem amtlichen Formular, etwa eine Geburtsurkunde, behält seine Personen — das Kind und beide Eltern
|
|
25
|
+
mit ihrem Anteil, auch ein gesperrt getippter Name und ein Geburtsname nach „geb.“ —, und ein früher gelesenes
|
|
26
|
+
Dokument zeigt sie nach dem Update, ohne erneut gelesen zu werden. Jede Person und jedes Ereignis kommt mit der Stelle
|
|
27
|
+
auf der Seite, an der es steht, so zeigt die Strom-App von jedem
|
|
28
|
+
einen eigenen Ausschnitt.
|
|
26
29
|
In den Stammbaum gelangt nichts von selbst. Ein schon gelesenes Dokument sagt es, so wird es nicht versehentlich ein
|
|
27
30
|
zweites Mal gelesen. Ein erneut gelesenes Dokument wird unter dem Namen seiner Datei gezeigt, nie unter einem Code.
|
|
28
31
|
Ein aus der Strom-App gesendetes Dokument findet den Agenten wie das Terminal — auch einen in einem eigenen Ordner,
|
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
7
7
|
|
|
8
8
|
## 1.18.0
|
|
9
9
|
|
|
10
|
-
*In preparation: the betas up to 1.18.0-beta.
|
|
10
|
+
*In preparation: the betas up to 1.18.0-beta.14. The section is complete with the release.*
|
|
11
11
|
|
|
12
12
|
- **Phone photos upright.** A photo turned by its EXIF orientation is read as it is meant to be seen, upright; the
|
|
13
13
|
places marked on it (excerpts, the halves of a spread, a cut-out part) are of the picture as it is shown. The
|
|
@@ -22,7 +22,9 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
22
22
|
name the document does not have is left out — a surname too, never taken from a father or another family —, what
|
|
23
23
|
cannot be read is marked; a second reader can read it blind. A
|
|
24
24
|
document on a printed form, as a birth certificate, keeps its people — the child and both parents with their part in
|
|
25
|
-
it
|
|
25
|
+
it, their names typed letter-spaced and a mother's birth name after "née" or "roz." too —, and a document read before
|
|
26
|
+
shows them after the update with nothing read again. Each person and event it names comes with the place on the page
|
|
27
|
+
where it stands, so the Strom app shows each its own excerpt. Nothing of it enters the tree by itself. A document read
|
|
26
28
|
already says so, so it is not read a second time by mistake. A document read again is shown under the name of its
|
|
27
29
|
file, never a code. A document sent from the Strom app finds the agent as the terminal does — one installed in a
|
|
28
30
|
folder of its own, by npm or Homebrew too — instead of saying no agent is set up. Every agent the research works with
|
package/dist/core/document.js
CHANGED
|
@@ -110,13 +110,17 @@ term: <a word or abbreviation as written> = <what it means, in ${into}> (term |
|
|
|
110
110
|
— one line each, for the old words, formulas, abbreviations, occupations, and on a gravestone its symbols
|
|
111
111
|
`}person: <ref> | <role> | ${blind ? "" : `<the role in ${into}> | `}written=<the name exactly as it stands in the transcript> | given=<the given
|
|
112
112
|
name in today's form> | surname=<the surname in today's form> | sex=M|F|U | age=<as written> | occupation=<…> |
|
|
113
|
-
residence=<…> — one line each person named; ref p1, p2, … in this entry; role: ${ROLES.join(", ")};
|
|
113
|
+
residence=<…> | region=x,y,w,h — one line each person named; ref p1, p2, … in this entry; role: ${ROLES.join(", ")};
|
|
114
114
|
given and surname only turn the written name into today's form ("Joannes" → "Jan"): never a name the transcript
|
|
115
115
|
does not have — no surname the entry does not write by the person (a child's is not taken from its father's);
|
|
116
116
|
on a form (a certificate) whose fields hold a name apart, written= the words of its fields as written there;
|
|
117
|
-
a name
|
|
117
|
+
a name in letter-spaced type ("N o v á k") is one word: surname=Novák; a woman written with her birth name
|
|
118
|
+
("Marie Nováková roz. Dvořáková", geb., née, z domu): surname= the birth name;
|
|
119
|
+
a name you could not read stays out; leave out what the entry does not say; region: where the person's name
|
|
120
|
+
stands — its line, or its field on a form — as the entry's region, of the page's whole view (tight, not the entry)
|
|
118
121
|
event: <BIRT | BAPM | MARR | DEAT | BURI | RESI | EVEN> | date=<as on the record, in numbers: 3.4.1846, or a year> |
|
|
119
|
-
place=<the place as written> | persons=<ref>:<principal|father|mother|godparent|witness|groom|bride|deceased|other>,…
|
|
122
|
+
place=<the place as written> | persons=<ref>:<principal|father|mother|godparent|witness|groom|bride|deceased|other>,… |
|
|
123
|
+
region=x,y,w,h — where its date (and place) stand, as a person's region
|
|
120
124
|
relation: parent <ref of the parent> <ref of the child> — or — spouse <ref> <ref>
|
|
121
125
|
${blind
|
|
122
126
|
? ""
|
|
@@ -260,12 +264,18 @@ export function parseDocReport(text) {
|
|
|
260
264
|
const ref = /^p\d+$/iu.test(parts[0].trim()) ? parts.shift().trim().toLowerCase() : `p${entry.persons.length + 1}`;
|
|
261
265
|
const role = parts[0] && !parts[0].includes("=") ? parts.shift().trim().toLowerCase() : "other";
|
|
262
266
|
const roleText = parts[0] && !parts[0].includes("=") ? parts.shift().trim() : undefined;
|
|
263
|
-
|
|
267
|
+
const fields = pairs(parts);
|
|
268
|
+
const region = parseRegion(fields.region);
|
|
269
|
+
delete fields.region;
|
|
270
|
+
entry.persons.push({ ref, role, ...(roleText ? { roleText } : {}), ...(region ? { region } : {}), fields });
|
|
264
271
|
}
|
|
265
272
|
for (const e of fields.get("event") ?? []) {
|
|
266
273
|
const parts = e.split(/\s*\|\s*/u);
|
|
267
274
|
const type = parts[0] && !parts[0].includes("=") ? parts.shift().trim() : "EVEN";
|
|
268
|
-
|
|
275
|
+
const fields = pairs(parts);
|
|
276
|
+
const region = parseRegion(fields.region);
|
|
277
|
+
delete fields.region;
|
|
278
|
+
entry.events.push({ type, ...(region ? { region } : {}), fields });
|
|
269
279
|
}
|
|
270
280
|
for (const r of fields.get("relation") ?? []) {
|
|
271
281
|
const w = r.split(/[\s,]+/u).filter(Boolean);
|
|
@@ -304,20 +314,113 @@ function kindOf(said) {
|
|
|
304
314
|
return "photo.back";
|
|
305
315
|
return "other";
|
|
306
316
|
}
|
|
307
|
-
|
|
317
|
+
/** One letter alone, as a typewriter spaces out a name (V í š e k); the last of them may have a stop or a "[?]". */
|
|
318
|
+
const SPACED = /^\p{L}\p{M}*$/u;
|
|
319
|
+
const SPACED_LAST = /^\p{L}\p{M}*(?:\[\?\]|[^\p{L}\p{M}\p{N}\[\]])*$/u;
|
|
320
|
+
const keyOf = (t) => foldText(t.replace(/\[\?\]/gu, "")).replace(/[^\p{L}\p{M}\p{N}]+/gu, "");
|
|
321
|
+
/** The keys of the words of names (a reader's given name and surname, the other reader's text) that join spaced letters. */
|
|
322
|
+
export function nameKeys(...names) {
|
|
323
|
+
return names.flatMap((n) => (n ? words(n).map((w) => w.key) : [])).filter((k) => k.length >= 2 && k !== "[...]");
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The words of a text as they are compared: a word after a stop with no space ("roz.Nováková", "n.Labem") a word of
|
|
327
|
+
* its own, glued to the one before; letters standing alone (a name in letter-spaced type, "N o v á k") one word, as
|
|
328
|
+
* written — a run of them that spells one of `names` (the reader's given name or surname), the rest of a run that has
|
|
329
|
+
* one, and else three or more beginning with a capital; a gap of two spaces or more (a typewriter's between the words of
|
|
330
|
+
* a spaced name) or a line ends a run. Letters of running text ("šel a v k domu") stay words of their own.
|
|
331
|
+
*/
|
|
332
|
+
export function words(s, names = []) {
|
|
308
333
|
const src = s
|
|
309
334
|
.replace(/\s*\[\?\]/gu, "[?]")
|
|
310
335
|
.replace(/\[\s*(?:\.{2,}|…)\s*\]/gu, " [...] ")
|
|
311
|
-
.replace(/\|/gu, "
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
336
|
+
.replace(/\|/gu, " ");
|
|
337
|
+
const parts = [];
|
|
338
|
+
for (const m of src.matchAll(/(\s*)(\S+)/gu)) {
|
|
339
|
+
const wide = m[1].length > 1 || /[\n\r]/u.test(m[1]);
|
|
340
|
+
if (m[2] === "[...]")
|
|
341
|
+
parts.push({ t: m[2], glued: false, wide });
|
|
342
|
+
else
|
|
343
|
+
m[2].split(/(?<=\p{L}\p{M}*\.)(?=\p{L})/u).forEach((t, i) => parts.push({ t, glued: i > 0, wide: i === 0 && wide }));
|
|
344
|
+
}
|
|
345
|
+
const known = new Set(names);
|
|
346
|
+
const out = [];
|
|
347
|
+
const letters = (a, b) => {
|
|
348
|
+
const text = parts.slice(a, b).map((x) => x.t).join(" ");
|
|
349
|
+
out.push({ text, key: keyOf(text), unsure: text.includes("[?]"), illegible: false });
|
|
350
|
+
};
|
|
351
|
+
for (let i = 0; i < parts.length; i++) {
|
|
352
|
+
// a run of letters alone: no gap of two spaces within, the last may have a stop, the word after not glued to it
|
|
353
|
+
let j = i;
|
|
354
|
+
while (j < parts.length && !parts[j].glued && (j === i || !parts[j].wide) && SPACED.test(parts[j].t))
|
|
355
|
+
j++;
|
|
356
|
+
if (j < parts.length && j > i && !parts[j].glued && !parts[j].wide && SPACED_LAST.test(parts[j].t) && !SPACED.test(parts[j].t))
|
|
357
|
+
j++;
|
|
358
|
+
if (j - i >= 2 && (j === parts.length || !parts[j].glued)) {
|
|
359
|
+
// the names it spells, the longest first, and what stands between them
|
|
360
|
+
const named = [];
|
|
361
|
+
for (let a = i; a < j - 1;) {
|
|
362
|
+
let b = j;
|
|
363
|
+
while (b - a >= 2 && !known.has(keyOf(parts.slice(a, b).map((x) => x.t).join(""))))
|
|
364
|
+
b--;
|
|
365
|
+
if (b - a >= 2) {
|
|
366
|
+
named.push([a, b]);
|
|
367
|
+
a = b;
|
|
368
|
+
}
|
|
369
|
+
else
|
|
370
|
+
a++;
|
|
371
|
+
}
|
|
372
|
+
if (named.length) {
|
|
373
|
+
let a = i;
|
|
374
|
+
for (const [x, y] of [...named, [j, j]]) {
|
|
375
|
+
if (x - a >= 2)
|
|
376
|
+
letters(a, x);
|
|
377
|
+
else
|
|
378
|
+
for (let k = a; k < x; k++)
|
|
379
|
+
letters(k, k + 1);
|
|
380
|
+
if (y > x)
|
|
381
|
+
letters(x, y);
|
|
382
|
+
a = y;
|
|
383
|
+
}
|
|
384
|
+
i = j - 1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (j - i >= 3 && /^\p{Lu}/u.test(parts[i].t)) {
|
|
388
|
+
letters(i, j);
|
|
389
|
+
i = j - 1;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const { t, glued } = parts[i];
|
|
394
|
+
if (t === "[...]") {
|
|
395
|
+
out.push({ text: t, key: "[...]", unsure: false, illegible: true });
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const key = keyOf(t);
|
|
399
|
+
if (key)
|
|
400
|
+
out.push({ text: t, key, unsure: t.includes("[?]"), illegible: false, ...(glued ? { glued } : {}) });
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
/** Words that stand in a row as they were written: one space between, none before a word glued to the one before. */
|
|
405
|
+
function asWritten(span) {
|
|
406
|
+
return span.map((x, i) => (i && !x.glued ? ` ${x.text}` : x.text)).join("");
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The words of a birth name's mark, folded: in full — rozená, geborene, née, nata, urodzona, урождённая, z domu — or
|
|
410
|
+
* shortened with its stop — roz., geb., urodz., ur., урожд. —: the name after one is the person's birth surname.
|
|
411
|
+
*/
|
|
412
|
+
const BIRTH_WORDS = new Set(["rozena", "rozeny", "geborene", "geborener", "nee", "nata", "urodzona", "урожденная", "урожденный", "szuletett"]);
|
|
413
|
+
const BIRTH_SHORT = new Set(["roz", "geb", "urodz", "ur", "урожд", "szul"]);
|
|
414
|
+
/** A birth name's mark at `i`; a short one only with its stop, "ur." only before the reader's surname (Ur is a name too). */
|
|
415
|
+
function birthMarkAt(span, i, surnameAt) {
|
|
416
|
+
const x = span[i];
|
|
417
|
+
if (x.key === "domu")
|
|
418
|
+
return span[i - 1]?.key === "z";
|
|
419
|
+
if (BIRTH_WORDS.has(x.key))
|
|
420
|
+
return true;
|
|
421
|
+
if (!BIRTH_SHORT.has(x.key) || !/\.$/u.test(x.text.replace(/\[\?\]/gu, "")))
|
|
422
|
+
return false;
|
|
423
|
+
return x.key !== "ur" || i + 1 === surnameAt;
|
|
321
424
|
}
|
|
322
425
|
/** Where the words of `part` stand in `whole`, in a row (its first place), else -1. */
|
|
323
426
|
function find(whole, part) {
|
|
@@ -504,17 +607,20 @@ export function checkEntry(raw, id, blind = false, form = false) {
|
|
|
504
607
|
checks.push({ entry: id, ref: p.ref, what: [p.fields.given, p.fields.surname].filter(Boolean).join(" ") || p.role, why: "no-name" });
|
|
505
608
|
continue;
|
|
506
609
|
}
|
|
507
|
-
|
|
508
|
-
const
|
|
610
|
+
// letters spaced out (a name typed letter-spaced) joined as the reader's given name and surname spell them
|
|
611
|
+
const names = nameKeys(p.fields.given, p.fields.surname);
|
|
612
|
+
const own = words(raw.transcript, names);
|
|
613
|
+
const w = words(written, names);
|
|
614
|
+
const at = find(own, w);
|
|
509
615
|
// a form (a certificate) holds a name in its fields apart — the surname in one, the given names in another —:
|
|
510
616
|
// its words each written there are the name; elsewhere a name stands in a row as written
|
|
511
|
-
const span = at >= 0 ?
|
|
617
|
+
const span = at >= 0 ? own.slice(at, at + w.length) : form ? findApart(own, w) : undefined;
|
|
512
618
|
if (!span) {
|
|
513
619
|
checks.push({ entry: id, ref: p.ref, what: written, why: "not-in-transcript" });
|
|
514
620
|
continue;
|
|
515
621
|
}
|
|
516
622
|
const role = (ROLES.includes(p.role) ? p.role : "other");
|
|
517
|
-
const person = { ref: p.ref, role, written: span.map((x) => x.text).join(" ") };
|
|
623
|
+
const person = { ref: p.ref, role, written: at >= 0 ? asWritten(span) : span.map((x) => x.text).join(" ") };
|
|
518
624
|
if (p.roleText && !blind)
|
|
519
625
|
person.roleText = p.roleText;
|
|
520
626
|
const uncertain = new Set();
|
|
@@ -540,7 +646,13 @@ export function checkEntry(raw, id, blind = false, form = false) {
|
|
|
540
646
|
}
|
|
541
647
|
// the given name: the other words; a word of today's form must be one of them in its letters (Josephi → Josef,
|
|
542
648
|
// Joannes → Jan), else the name is as written and the reader's form only a lead (today)
|
|
543
|
-
|
|
649
|
+
// (not a birth name's mark, roz. or geb., nor the birth name after it)
|
|
650
|
+
const marked = new Set();
|
|
651
|
+
span.forEach((x, i) => {
|
|
652
|
+
if (birthMarkAt(span, i, surnameAt))
|
|
653
|
+
[i, i + 1, ...(x.key === "domu" ? [i - 1] : [])].forEach((k) => marked.add(k));
|
|
654
|
+
});
|
|
655
|
+
const givenWords = span.filter((_, i) => i !== surnameAt && !marked.has(i));
|
|
544
656
|
const given = p.fields.given?.replace(/\[\?\]/gu, "").trim();
|
|
545
657
|
if (given) {
|
|
546
658
|
const legible = givenWords.filter((x) => !x.illegible);
|
|
@@ -551,7 +663,7 @@ export function checkEntry(raw, id, blind = false, form = false) {
|
|
|
551
663
|
if (fits)
|
|
552
664
|
person.given = given;
|
|
553
665
|
else {
|
|
554
|
-
person.given = legible.map((x) => x.text.replace(/\[\?\]
|
|
666
|
+
person.given = legible.map((x) => x.text.replace(/\[\?\]|\s+/gu, "").replace(/^[^\p{L}\p{M}\p{N}]+|[^\p{L}\p{M}\p{N}]+$/gu, "")).join(" ");
|
|
555
667
|
person.today = given;
|
|
556
668
|
uncertain.add("given");
|
|
557
669
|
checks.push({ entry: id, ref: p.ref, what: `given "${given}" for "${person.given}"`, why: "not-of-its-letters" });
|
|
@@ -574,6 +686,9 @@ export function checkEntry(raw, id, blind = false, form = false) {
|
|
|
574
686
|
}
|
|
575
687
|
if (uncertain.size)
|
|
576
688
|
person.uncertain = [...uncertain];
|
|
689
|
+
const region = partOf(p.region, raw.region);
|
|
690
|
+
if (region)
|
|
691
|
+
person.region = region;
|
|
577
692
|
persons.push(person);
|
|
578
693
|
}
|
|
579
694
|
const refs = new Set(persons.map((p) => p.ref));
|
|
@@ -603,6 +718,9 @@ export function checkEntry(raw, id, blind = false, form = false) {
|
|
|
603
718
|
}
|
|
604
719
|
if (uncertain.size)
|
|
605
720
|
ev.uncertain = [...uncertain];
|
|
721
|
+
const region = partOf(e.region, raw.region);
|
|
722
|
+
if (region)
|
|
723
|
+
ev.region = region;
|
|
606
724
|
events.push(ev);
|
|
607
725
|
}
|
|
608
726
|
const relations = [];
|
|
@@ -623,11 +741,30 @@ export function checkEntry(raw, id, blind = false, form = false) {
|
|
|
623
741
|
entry.glossary = raw.terms;
|
|
624
742
|
return { entry, checks };
|
|
625
743
|
}
|
|
744
|
+
/** A name's or a date's line is no more than a quarter of the page. */
|
|
745
|
+
const PART_AREA = 0.25;
|
|
746
|
+
/** How far a part may lie out of its entry's region (a reader's regions are rough). */
|
|
747
|
+
const PART_SLACK = 0.05;
|
|
748
|
+
/**
|
|
749
|
+
* A person's or an event's region as a reader gave it, where it can be one: a part of the page no bigger than a
|
|
750
|
+
* quarter of it, that lies (roughly) in its entry's region; else none — the app shows the entry's region then.
|
|
751
|
+
*/
|
|
752
|
+
function partOf(r, entry) {
|
|
753
|
+
if (!r || r[2] * r[3] > PART_AREA)
|
|
754
|
+
return undefined;
|
|
755
|
+
if (!entry)
|
|
756
|
+
return r;
|
|
757
|
+
const [x, y, w, h] = [entry[0] - PART_SLACK, entry[1] - PART_SLACK, entry[2] + 2 * PART_SLACK, entry[3] + 2 * PART_SLACK];
|
|
758
|
+
const across = Math.min(r[0] + r[2], x + w) - Math.max(r[0], x);
|
|
759
|
+
const down = Math.min(r[1] + r[3], y + h) - Math.max(r[1], y);
|
|
760
|
+
return across > 0 && down > 0 ? r : undefined;
|
|
761
|
+
}
|
|
626
762
|
// ── two readers ───────────────────────────────────────────────────────────────────────────────────────────────────
|
|
627
763
|
/** The hunks where two transcripts differ, word by word (their keys; the marks [?] are no difference). */
|
|
628
764
|
export function wordDiff(a, b) {
|
|
629
|
-
|
|
630
|
-
const
|
|
765
|
+
// letters one reader spaced out joined as the other wrote the word
|
|
766
|
+
const x = words(a, nameKeys(b));
|
|
767
|
+
const y = words(b, nameKeys(a));
|
|
631
768
|
const n = x.length;
|
|
632
769
|
const m = y.length;
|
|
633
770
|
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
@@ -667,7 +804,7 @@ const same = (a, b) => foldText(a ?? "") === foldText(b ?? "");
|
|
|
667
804
|
* one's. Index in `mine` → index in `theirs`.
|
|
668
805
|
*/
|
|
669
806
|
function pairByName(mine, theirs) {
|
|
670
|
-
const keys = (p) => words(p.written).filter((x) => !x.illegible).map((x) => x.key);
|
|
807
|
+
const keys = (p) => words(p.written, nameKeys(p.given, p.surname)).filter((x) => !x.illegible).map((x) => x.key);
|
|
671
808
|
const surname = (p) => words(p.surname ?? "").map((x) => x.key);
|
|
672
809
|
const WHOLE = 1000;
|
|
673
810
|
const score = (p, q) => {
|
|
@@ -720,6 +857,7 @@ export function compareReadings(first, blind) {
|
|
|
720
857
|
used.set(e.page, k + 1);
|
|
721
858
|
const b = other.get(e.page)?.[k];
|
|
722
859
|
const variants = [];
|
|
860
|
+
const regions = new Map();
|
|
723
861
|
if (!b)
|
|
724
862
|
variants.push({ field: "entry", at: e.id, other: "" });
|
|
725
863
|
else {
|
|
@@ -737,6 +875,9 @@ export function compareReadings(first, blind) {
|
|
|
737
875
|
for (const f of ["given", "surname"])
|
|
738
876
|
if (!same(p[f], q[f]))
|
|
739
877
|
variants.push({ field: `${p.ref}.${f}`, at: p[f] ?? "", other: q[f] ?? "" });
|
|
878
|
+
// where the name stands: the first reader's, else the blind one's of the same person
|
|
879
|
+
if (!p.region && q.region)
|
|
880
|
+
regions.set(p, q.region);
|
|
740
881
|
};
|
|
741
882
|
const alone = [];
|
|
742
883
|
for (const p of e.persons) {
|
|
@@ -775,6 +916,8 @@ export function compareReadings(first, blind) {
|
|
|
775
916
|
variants.push({ field: ev.type, at: ev.date?.text ?? ev.type, other: "" });
|
|
776
917
|
continue;
|
|
777
918
|
}
|
|
919
|
+
if (!ev.region && q.region)
|
|
920
|
+
regions.set(ev, q.region);
|
|
778
921
|
if ((ev.date?.gedcom ?? ev.date?.text) !== (q.date?.gedcom ?? q.date?.text) && !same(ev.date?.text, q.date?.text))
|
|
779
922
|
variants.push({ field: `${ev.type}.date`, at: ev.date?.text ?? "", other: q.date?.text ?? "" });
|
|
780
923
|
if (!same(ev.place?.text, q.place?.text))
|
|
@@ -784,7 +927,8 @@ export function compareReadings(first, blind) {
|
|
|
784
927
|
const agree = variants.length ? "differ" : "same";
|
|
785
928
|
if (variants.length)
|
|
786
929
|
all = false;
|
|
787
|
-
|
|
930
|
+
const placed = (x) => (regions.has(x) ? { ...x, region: regions.get(x) } : x);
|
|
931
|
+
return { ...e, persons: e.persons.map(placed), events: e.events.map(placed), agree, ...(variants.length ? { variants } : {}) };
|
|
788
932
|
});
|
|
789
933
|
// entries only the blind reader has
|
|
790
934
|
for (const [page, list] of other)
|
|
@@ -820,7 +964,7 @@ function bornRange(role, year, age) {
|
|
|
820
964
|
* and a surname of the same family (Novák/Nováková) are needed; the same sex, a birth year that fits the role and the
|
|
821
965
|
* record's year, and a place of theirs add to it. A suggestion, never a decision: the person chooses.
|
|
822
966
|
*/
|
|
823
|
-
export function matchPerson(tree, p, year, place) {
|
|
967
|
+
export function matchPerson(tree, p, year, place, own, child) {
|
|
824
968
|
if (!p.given || !p.surname)
|
|
825
969
|
return [];
|
|
826
970
|
const firstOf = (s) => foldText(s).split(" ")[0];
|
|
@@ -849,8 +993,16 @@ export function matchPerson(tree, p, year, place) {
|
|
|
849
993
|
score += 0.1;
|
|
850
994
|
}
|
|
851
995
|
const born = dateYears(birthEvent(o)?.date ?? "")[0];
|
|
852
|
-
|
|
853
|
-
|
|
996
|
+
// a parent of one the document says was born then (a certificate's grandparents) as a parent of that year
|
|
997
|
+
const range = child !== undefined ? bornRange("father", child) : year !== undefined ? bornRange(p.role, year, p.age) : undefined;
|
|
998
|
+
// the year the document says the person was born (a certificate's parents): the tree's, a year apart at most
|
|
999
|
+
if (own !== undefined && born !== undefined) {
|
|
1000
|
+
if (Math.abs(born - own) > 1)
|
|
1001
|
+
continue;
|
|
1002
|
+
why.push("years");
|
|
1003
|
+
score += 0.15;
|
|
1004
|
+
}
|
|
1005
|
+
else if (range && born !== undefined) {
|
|
854
1006
|
if (born < range[0] - 2 || born > range[1] + 2)
|
|
855
1007
|
continue;
|
|
856
1008
|
why.push("years");
|
|
@@ -884,8 +1036,14 @@ export function withTree(tree, entries) {
|
|
|
884
1036
|
const known = knownPlace(tree, x.place.text);
|
|
885
1037
|
return known ? { ...x, place: { ...x.place, known } } : x;
|
|
886
1038
|
});
|
|
1039
|
+
// the year each person was born, where the document says so
|
|
1040
|
+
const bornOf = (ref) => {
|
|
1041
|
+
const birth = e.events.find((x) => ["BIRT", "BAPM", "CHR"].includes(x.type) && x.date?.gedcom && x.persons.some((r) => r.ref === ref && r.role === "principal"));
|
|
1042
|
+
return birth?.date?.gedcom ? dateYears(birth.date.gedcom)[0] : undefined;
|
|
1043
|
+
};
|
|
887
1044
|
const persons = e.persons.map((p) => {
|
|
888
|
-
const
|
|
1045
|
+
const child = e.relations.flatMap((r) => (r.type === "parent" && r.parent === p.ref ? [bornOf(r.child)] : [])).find((y) => y !== undefined);
|
|
1046
|
+
const match = matchPerson(tree, p, year, place, bornOf(p.ref), child);
|
|
889
1047
|
return match.length ? { ...p, match } : p;
|
|
890
1048
|
});
|
|
891
1049
|
return { ...e, persons, events };
|
|
@@ -1045,8 +1203,14 @@ export function buildResult(tree, s) {
|
|
|
1045
1203
|
checks.push(...c.checks);
|
|
1046
1204
|
// a region of a part asked (--region): of the whole page
|
|
1047
1205
|
const page = s.pages.find((p) => p.n === e.page);
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1206
|
+
if (page?.region) {
|
|
1207
|
+
const part = page.region;
|
|
1208
|
+
if (c.entry.region)
|
|
1209
|
+
c.entry.region = within(part, c.entry.region);
|
|
1210
|
+
for (const x of [...c.entry.persons, ...c.entry.events])
|
|
1211
|
+
if (x.region)
|
|
1212
|
+
x.region = within(part, x.region);
|
|
1213
|
+
}
|
|
1050
1214
|
out.push(c.entry);
|
|
1051
1215
|
}
|
|
1052
1216
|
});
|
package/dist/core/tree.js
CHANGED
|
@@ -27,7 +27,7 @@ import { lastBackup } from "./backup.js";
|
|
|
27
27
|
export const TREE_FILE = "strom.json";
|
|
28
28
|
// between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
|
|
29
29
|
// at a release the release's own (package.json)
|
|
30
|
-
export const VERSION = "1.18.0-beta.
|
|
30
|
+
export const VERSION = "1.18.0-beta.14";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|