@finbheara/names 0.12.0 → 0.13.0

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/README.md CHANGED
@@ -142,6 +142,36 @@ The verdict is a declaration the caller makes about a document; grouping rows in
142
142
  documents is the caller's. `bun run names order <file>` prints the same for a file of
143
143
  name cells, one per line. The specification is `docs/NAME-NORMALIZATION.md`.
144
144
 
145
+ ### How two names are related
146
+
147
+ ```ts
148
+ import { createNameIndex, createNameKinship, givenFamilyTable, nameKinship, personName }
149
+ from "@finbheara/names/normalize";
150
+
151
+ nameKinship(personName("Kate Smith"), personName("Katherine Smith"));
152
+ // { relation: "candidate", via: ["given-family"], reason: "middles-equal", gateSafe: true }
153
+ nameKinship(personName("Mary Larson"), personName("Mary Conley-Larson")).via; // ["surname-part"]
154
+ nameKinship(personName("Mary Larson"), personName("Mary Carlson")).reason; // "surname-differs"
155
+ nameKinship(personName("Kate M Smith"), personName("Katherine Conley-Smith")).via;
156
+ // ["surname-part", "given-family", "middle-absent"]
157
+
158
+ // the given-name tolerance is a list of providers, asked in order
159
+ const withMaybe = createNameKinship({
160
+ given: [givenFamilyTable, (a, b) => (a === "EVIE" && b === "GENEVIEVE" ? "given-maybe" : null)],
161
+ });
162
+ withMaybe(personName("Evie Smith"), personName("Genevieve Smith"));
163
+ // { relation: "candidate", via: ["given-maybe"], reason: "middles-equal", gateSafe: false }
164
+
165
+ const index = createNameIndex<number>("nn3"); // a NameMap with a tolerant lookup
166
+ index.set(personName("Mary Conley-Larson"), 1).set(personName("Mary Carlson"), 2);
167
+ index.get(personName("Mary Larson")); // undefined: get stays exact
168
+ index.related(personName("Mary Larson")); // [{ name: Conley-Larson<<Mary, value: 1, kinship: … }]
169
+ ```
170
+
171
+ `candidate` is never a merge: it says two prints MAY be one person. `via` lists every
172
+ tolerance the pair used, and `gateSafe` is false when any of them is one a gate must not
173
+ admit on its own.
174
+
145
175
  ## Phonetic and blocking keys
146
176
 
147
177
  ```ts
@@ -262,11 +262,28 @@ var ROOT = (() => {
262
262
  })();
263
263
  var NOT_AT_GATE = new Set(TABLE.notAtGate);
264
264
  function sameGivenFamily2(a, b) {
265
- if (a === b || NOT_AT_GATE.has(a) || NOT_AT_GATE.has(b))
266
- return false;
265
+ return givenFamilyTable2(a, b) === "given-family";
266
+ }
267
+ function givenFamilyTable2(a, b) {
268
+ if (a === b)
269
+ return null;
267
270
  const ra = ROOT.get(a);
268
- return ra !== undefined && ra === ROOT.get(b);
271
+ if (ra === undefined || ra !== ROOT.get(b))
272
+ return null;
273
+ return NOT_AT_GATE.has(a) || NOT_AT_GATE.has(b) ? "given-family-not-gate" : "given-family";
269
274
  }
275
+ // src/normalize/multiset.ts
276
+ function subMultiset(outer, inner) {
277
+ const left = [...inner];
278
+ for (const part of outer) {
279
+ const at = left.indexOf(part);
280
+ if (at < 0)
281
+ return false;
282
+ left.splice(at, 1);
283
+ }
284
+ return true;
285
+ }
286
+
270
287
  // src/normalize/name-middle.ts
271
288
  function suffixCut(p) {
272
289
  return Math.max(1, p.givenFields().length - p.suffixes.length);
@@ -298,6 +315,9 @@ function nameRelation2(a, b) {
298
315
  if (a.surnameKey() !== b.surnameKey() || ga[0] !== gb[0] || ga[0] === undefined) {
299
316
  return { relation: "different", reason: "head-differs" };
300
317
  }
318
+ return tailRelation(a, b);
319
+ }
320
+ function tailRelation(a, b) {
301
321
  if (!sameList(suffixFields(a), suffixFields(b))) {
302
322
  return { relation: "different", reason: "suffixes-differ" };
303
323
  }
@@ -318,138 +338,6 @@ function nameRelation2(a, b) {
318
338
  return { relation: "candidate", reason: "initial-agrees" };
319
339
  }
320
340
 
321
- // src/normalize/name-compare.ts
322
- function nameCompare2(a, b, dop) {
323
- const x = levelOf2(a, dop);
324
- const y = levelOf2(b, dop);
325
- if (x < y)
326
- return -1;
327
- return x > y ? 1 : 0;
328
- }
329
- function probablySame2(a, b, dop) {
330
- if (!a.isPerson() || !b.isPerson())
331
- return false;
332
- if (dop === "nn5")
333
- return nameRelation2(a, b).relation !== "different";
334
- return levelOf2(a, dop) === levelOf2(b, dop);
335
- }
336
- function slotOf(name, dop) {
337
- return name.isPerson() ? levelOf2(name, dop) : null;
338
- }
339
- var loner = 0;
340
- var lonerSlot = () => `\x1F\x1F${loner++}`;
341
- function createNameSet2(dop) {
342
- const held = new Map;
343
- const set = {
344
- dop,
345
- get size() {
346
- return held.size;
347
- },
348
- get: (name) => {
349
- const slot = slotOf(name, dop);
350
- return slot === null ? undefined : held.get(slot);
351
- },
352
- has: (name) => {
353
- const slot = slotOf(name, dop);
354
- return slot === null ? false : held.has(slot);
355
- },
356
- add(name) {
357
- const slot = slotOf(name, dop);
358
- if (slot === null)
359
- held.set(lonerSlot(), name);
360
- else if (!held.has(slot))
361
- held.set(slot, name);
362
- return this;
363
- },
364
- delete: (name) => {
365
- const slot = slotOf(name, dop);
366
- return slot === null ? false : held.delete(slot);
367
- },
368
- clear: () => held.clear(),
369
- values: () => held.values(),
370
- [Symbol.iterator]: () => held.values()
371
- };
372
- return set;
373
- }
374
- function createNameMap2(dop, entries) {
375
- const held = new Map;
376
- const map = {
377
- dop,
378
- get size() {
379
- return held.size;
380
- },
381
- get: (name) => {
382
- const key = slotOf(name, dop);
383
- return key === null ? undefined : held.get(key)?.[1];
384
- },
385
- has: (name) => {
386
- const key = slotOf(name, dop);
387
- return key === null ? false : held.has(key);
388
- },
389
- set(name, value) {
390
- const key = slotOf(name, dop);
391
- const slot = key === null ? undefined : held.get(key);
392
- if (slot === undefined)
393
- held.set(key ?? lonerSlot(), [name, value]);
394
- else
395
- slot[1] = value;
396
- return this;
397
- },
398
- delete: (name) => {
399
- const key = slotOf(name, dop);
400
- return key === null ? false : held.delete(key);
401
- },
402
- clear: () => held.clear(),
403
- *keys() {
404
- for (const [name] of held.values())
405
- yield name;
406
- },
407
- *values() {
408
- for (const [, value] of held.values())
409
- yield value;
410
- },
411
- *entries() {
412
- for (const slot of held.values())
413
- yield [slot[0], slot[1]];
414
- },
415
- [Symbol.iterator]() {
416
- return this.entries();
417
- }
418
- };
419
- for (const [name, value] of entries ?? [])
420
- map.set(name, value);
421
- return map;
422
- }
423
- function createNameClusters2(dop, linkage) {
424
- if (dop === "nn5") {
425
- throw new Error('createNameClusters at nn5 is unimplemented: a linkage is defined over a distance and NN5 answers a relation. Use nameRelation / probablySame(a, b, "nn5"); see docs/NAME-NORMALIZATION.md § NN5 — the middle-name relation.');
426
- }
427
- const transitive = dop;
428
- const held = new Map;
429
- const out = {
430
- dop,
431
- linkage,
432
- get size() {
433
- return held.size;
434
- },
435
- add(name) {
436
- const key = slotOf(name, transitive);
437
- const cluster = key === null ? undefined : held.get(key);
438
- if (cluster === undefined)
439
- held.set(key ?? lonerSlot(), [name]);
440
- else
441
- cluster.push(name);
442
- return this;
443
- },
444
- clusterOf: (name) => {
445
- const key = slotOf(name, transitive);
446
- return key === null ? undefined : held.get(key);
447
- },
448
- clusters: () => [...held.values()],
449
- [Symbol.iterator]: () => held.values()
450
- };
451
- return out;
452
- }
453
341
  // src/normalize/name-compose.ts
454
342
  var READER_RANK = ["as-parsed", "boundary", "order"];
455
343
  var VIA_ORDER = [
@@ -552,16 +440,6 @@ function meetMiddles(x, y) {
552
440
  }
553
441
  return out;
554
442
  }
555
- function subMultiset(outer, inner) {
556
- const left = [...inner];
557
- for (const part of outer) {
558
- const at = left.indexOf(part);
559
- if (at < 0)
560
- return false;
561
- left.splice(at, 1);
562
- }
563
- return true;
564
- }
565
443
  function meetSurname(x, y) {
566
444
  if (subMultiset(x, y))
567
445
  return y;
@@ -817,6 +695,305 @@ function foldUnder(prints, surname) {
817
695
  return mx - my || (slotKey(x) < slotKey(y) ? -1 : 1);
818
696
  })[0];
819
697
  }
698
+
699
+ // src/normalize/name-kinship.ts
700
+ var KINSHIP_VIAS2 = Object.freeze({
701
+ "surname-part": Object.freeze({ field: "surname", gateSafe: true }),
702
+ "given-family": Object.freeze({ field: "given", gateSafe: true }),
703
+ "given-family-not-gate": Object.freeze({ field: "given", gateSafe: false }),
704
+ "given-maybe": Object.freeze({ field: "given", gateSafe: false }),
705
+ "middle-absent": Object.freeze({ field: "middle", gateSafe: true }),
706
+ "initial-agrees": Object.freeze({ field: "middle", gateSafe: true })
707
+ });
708
+ var VIA_ORDER2 = Object.freeze(Object.keys(KINSHIP_VIAS2));
709
+ var different = (reason) => ({
710
+ relation: "different",
711
+ via: [],
712
+ reason,
713
+ gateSafe: false
714
+ });
715
+ function surnamePartOf(a, b) {
716
+ const pa = surnameParts2(a);
717
+ const pb = surnameParts2(b);
718
+ if (pa.length === pb.length)
719
+ return false;
720
+ const [short, long] = pa.length < pb.length ? [pa, pb] : [pb, pa];
721
+ return subMultiset(short, long);
722
+ }
723
+ function createNameKinship2(options = {}) {
724
+ const providers = Object.freeze([
725
+ ...options.given ?? [givenFamilyTable2]
726
+ ]);
727
+ const givenVia = (x, y) => {
728
+ const [lo, hi] = x < y ? [x, y] : [y, x];
729
+ for (const provider of providers) {
730
+ const via = provider(lo, hi);
731
+ if (via !== null)
732
+ return via;
733
+ }
734
+ return null;
735
+ };
736
+ return (a, b) => {
737
+ if (!a.isPerson() || !b.isPerson())
738
+ return different("not-a-person");
739
+ const found = new Set;
740
+ if (a.surnameKey() !== b.surnameKey()) {
741
+ if (!surnamePartOf(a, b))
742
+ return different("surname-differs");
743
+ found.add("surname-part");
744
+ }
745
+ const ga = a.givenFields()[0];
746
+ const gb = b.givenFields()[0];
747
+ if (ga === undefined || gb === undefined)
748
+ return different("given-differs");
749
+ if (ga !== gb) {
750
+ const via = givenVia(ga, gb);
751
+ if (via === null)
752
+ return different("given-differs");
753
+ found.add(via);
754
+ }
755
+ const tail = tailRelation(a, b);
756
+ if (tail.relation === "different")
757
+ return different(tail.reason);
758
+ if (tail.relation === "candidate")
759
+ found.add(tail.reason);
760
+ const via = VIA_ORDER2.filter((v) => found.has(v));
761
+ return {
762
+ relation: via.length === 0 ? "same" : "candidate",
763
+ via,
764
+ reason: tail.reason,
765
+ gateSafe: via.every((v) => KINSHIP_VIAS2[v].gateSafe)
766
+ };
767
+ };
768
+ }
769
+ var nameKinship2 = createNameKinship2();
770
+
771
+ // src/normalize/name-compare.ts
772
+ function nameCompare2(a, b, dop) {
773
+ const x = levelOf2(a, dop);
774
+ const y = levelOf2(b, dop);
775
+ if (x < y)
776
+ return -1;
777
+ return x > y ? 1 : 0;
778
+ }
779
+ function probablySame2(a, b, dop) {
780
+ if (!a.isPerson() || !b.isPerson())
781
+ return false;
782
+ if (dop === "nn5")
783
+ return nameRelation2(a, b).relation !== "different";
784
+ return levelOf2(a, dop) === levelOf2(b, dop);
785
+ }
786
+ function slotOf(name, dop) {
787
+ return name.isPerson() ? levelOf2(name, dop) : null;
788
+ }
789
+ var loner = 0;
790
+ var lonerSlot = () => `\x1F\x1F${loner++}`;
791
+ function createNameSet2(dop) {
792
+ const held = new Map;
793
+ const set = {
794
+ dop,
795
+ get size() {
796
+ return held.size;
797
+ },
798
+ get: (name) => {
799
+ const slot = slotOf(name, dop);
800
+ return slot === null ? undefined : held.get(slot);
801
+ },
802
+ has: (name) => {
803
+ const slot = slotOf(name, dop);
804
+ return slot === null ? false : held.has(slot);
805
+ },
806
+ add(name) {
807
+ const slot = slotOf(name, dop);
808
+ if (slot === null)
809
+ held.set(lonerSlot(), name);
810
+ else if (!held.has(slot))
811
+ held.set(slot, name);
812
+ return this;
813
+ },
814
+ delete: (name) => {
815
+ const slot = slotOf(name, dop);
816
+ return slot === null ? false : held.delete(slot);
817
+ },
818
+ clear: () => held.clear(),
819
+ values: () => held.values(),
820
+ [Symbol.iterator]: () => held.values()
821
+ };
822
+ return set;
823
+ }
824
+ function createNameMap2(dop, entries) {
825
+ const held = new Map;
826
+ const map = {
827
+ dop,
828
+ get size() {
829
+ return held.size;
830
+ },
831
+ get: (name) => {
832
+ const key = slotOf(name, dop);
833
+ return key === null ? undefined : held.get(key)?.[1];
834
+ },
835
+ has: (name) => {
836
+ const key = slotOf(name, dop);
837
+ return key === null ? false : held.has(key);
838
+ },
839
+ set(name, value) {
840
+ const key = slotOf(name, dop);
841
+ const slot = key === null ? undefined : held.get(key);
842
+ if (slot === undefined)
843
+ held.set(key ?? lonerSlot(), [name, value]);
844
+ else
845
+ slot[1] = value;
846
+ return this;
847
+ },
848
+ delete: (name) => {
849
+ const key = slotOf(name, dop);
850
+ return key === null ? false : held.delete(key);
851
+ },
852
+ clear: () => held.clear(),
853
+ *keys() {
854
+ for (const [name] of held.values())
855
+ yield name;
856
+ },
857
+ *values() {
858
+ for (const [, value] of held.values())
859
+ yield value;
860
+ },
861
+ *entries() {
862
+ for (const slot of held.values())
863
+ yield [slot[0], slot[1]];
864
+ },
865
+ [Symbol.iterator]() {
866
+ return this.entries();
867
+ }
868
+ };
869
+ for (const [name, value] of entries ?? [])
870
+ map.set(name, value);
871
+ return map;
872
+ }
873
+ function kinshipBuckets(name) {
874
+ return [...new Set([name.surnameKey(), ...surnameParts2(name)])];
875
+ }
876
+ var RELATION_RANK = {
877
+ same: 0,
878
+ candidate: 1,
879
+ different: 2
880
+ };
881
+ function createNameIndex2(dop, options = {}) {
882
+ const kinship = options.kinship ?? nameKinship2;
883
+ const map = createNameMap2(dop);
884
+ const filed = new Map;
885
+ const buckets = new Map;
886
+ let seq = 0;
887
+ const file = (slot, name) => {
888
+ filed.set(slot, { name, seq: seq++ });
889
+ for (const key of kinshipBuckets(name)) {
890
+ const slots = buckets.get(key);
891
+ if (slots === undefined)
892
+ buckets.set(key, new Set([slot]));
893
+ else
894
+ slots.add(slot);
895
+ }
896
+ };
897
+ const unfile = (slot) => {
898
+ const held = filed.get(slot);
899
+ if (held === undefined)
900
+ return;
901
+ filed.delete(slot);
902
+ for (const key of kinshipBuckets(held.name)) {
903
+ const slots = buckets.get(key);
904
+ slots?.delete(slot);
905
+ if (slots?.size === 0)
906
+ buckets.delete(key);
907
+ }
908
+ };
909
+ const index = {
910
+ dop,
911
+ get size() {
912
+ return map.size;
913
+ },
914
+ get: (name) => map.get(name),
915
+ has: (name) => map.has(name),
916
+ set(name, value) {
917
+ const slot = slotOf(name, dop);
918
+ if (slot !== null && !filed.has(slot))
919
+ file(slot, name);
920
+ map.set(name, value);
921
+ return this;
922
+ },
923
+ delete: (name) => {
924
+ const slot = slotOf(name, dop);
925
+ if (slot !== null)
926
+ unfile(slot);
927
+ return map.delete(name);
928
+ },
929
+ clear: () => {
930
+ filed.clear();
931
+ buckets.clear();
932
+ map.clear();
933
+ },
934
+ keys: () => map.keys(),
935
+ values: () => map.values(),
936
+ entries: () => map.entries(),
937
+ [Symbol.iterator]: () => map.entries(),
938
+ related(name) {
939
+ const slots = new Set;
940
+ for (const key of kinshipBuckets(name)) {
941
+ for (const slot of buckets.get(key) ?? [])
942
+ slots.add(slot);
943
+ }
944
+ const found = [];
945
+ for (const slot of slots) {
946
+ const held = filed.get(slot);
947
+ if (held === undefined)
948
+ continue;
949
+ const k = kinship(name, held.name);
950
+ if (k.relation === "different")
951
+ continue;
952
+ found.push({
953
+ name: held.name,
954
+ value: map.get(held.name),
955
+ kinship: k,
956
+ seq: held.seq
957
+ });
958
+ }
959
+ found.sort((x, y) => RELATION_RANK[x.kinship.relation] - RELATION_RANK[y.kinship.relation] || x.seq - y.seq);
960
+ return found.map(({ name: n, value, kinship: k }) => ({ name: n, value, kinship: k }));
961
+ }
962
+ };
963
+ for (const [name, value] of options.entries ?? [])
964
+ index.set(name, value);
965
+ return index;
966
+ }
967
+ function createNameClusters2(dop, linkage) {
968
+ if (dop === "nn5") {
969
+ throw new Error('createNameClusters at nn5 is unimplemented: a linkage is defined over a distance and NN5 answers a relation. Use nameRelation / probablySame(a, b, "nn5"); see docs/NAME-NORMALIZATION.md § NN5 — the middle-name relation.');
970
+ }
971
+ const transitive = dop;
972
+ const held = new Map;
973
+ const out = {
974
+ dop,
975
+ linkage,
976
+ get size() {
977
+ return held.size;
978
+ },
979
+ add(name) {
980
+ const key = slotOf(name, transitive);
981
+ const cluster = key === null ? undefined : held.get(key);
982
+ if (cluster === undefined)
983
+ held.set(key ?? lonerSlot(), [name]);
984
+ else
985
+ cluster.push(name);
986
+ return this;
987
+ },
988
+ clusterOf: (name) => {
989
+ const key = slotOf(name, transitive);
990
+ return key === null ? undefined : held.get(key);
991
+ },
992
+ clusters: () => [...held.values()],
993
+ [Symbol.iterator]: () => held.values()
994
+ };
995
+ return out;
996
+ }
820
997
  // src/normalize/name-infeasible.ts
821
998
  var INFEASIBLE_CLASSES2 = ["merge?", "spelling", "rule-gap"];
822
999
  function editDistance2(a, b) {
@@ -1420,7 +1597,7 @@ function deriveNameFormat2(prints, options = {}) {
1420
1597
  return first;
1421
1598
  return scoreReadings(cells.map((c) => readNameCell2(c, { ...options, placeColumn: true })), NAME_FORMAT_MODEL, threshold, schoolRead);
1422
1599
  }
1423
- export { repairMojibake2, sameGivenFamily2, comparePosition2, nameRelation2, nameCompare2, probablySame2, createNameSet2, createNameMap2, createNameClusters2, MAX_READINGS2, surnameParts2, printAttestsOrder2, nameReadings2, meetReading2, refinesReading2, sameReading2, chooseReadingPair2, composeReadings2, composeNames2, meetReadings2, nameMeet2, showReading2, blockKeysOfReadings2, nameBlockKeys2, namesFeasible2, INFEASIBLE_CLASSES2, editDistance2, nearSpelling2, compoundGiven2, middleForm2, infeasibleClass2, isListJoiner2, personList2, surnameBlock2, initialsBlock2, metaphoneBlocks2, NAME_LEVEL_COLUMNS2, NAME_COLUMNS2, nameColumns2, NAME_FORMATS2, nameFormatOrder2, ORDER_READINGS2, readNameCell2, DEFAULT_NAME_FORMAT_THRESHOLD2, deriveNameFormat2 };
1600
+ export { repairMojibake2, sameGivenFamily2, givenFamilyTable2, comparePosition2, nameRelation2, MAX_READINGS2, surnameParts2, printAttestsOrder2, nameReadings2, meetReading2, refinesReading2, sameReading2, chooseReadingPair2, composeReadings2, composeNames2, meetReadings2, nameMeet2, showReading2, blockKeysOfReadings2, nameBlockKeys2, namesFeasible2, KINSHIP_VIAS2, createNameKinship2, nameKinship2, nameCompare2, probablySame2, createNameSet2, createNameMap2, createNameIndex2, createNameClusters2, INFEASIBLE_CLASSES2, editDistance2, nearSpelling2, compoundGiven2, middleForm2, infeasibleClass2, isListJoiner2, personList2, surnameBlock2, initialsBlock2, metaphoneBlocks2, NAME_LEVEL_COLUMNS2, NAME_COLUMNS2, nameColumns2, NAME_FORMATS2, nameFormatOrder2, ORDER_READINGS2, readNameCell2, DEFAULT_NAME_FORMAT_THRESHOLD2, deriveNameFormat2 };
1424
1601
 
1425
- //# debugId=A39684B9543A024164756E2164756E21
1426
- //# sourceMappingURL=index-vqbvesky.js.map
1602
+ //# debugId=4B07A74825028F7364756E2164756E21
1603
+ //# sourceMappingURL=index-wq6kn9jq.js.map