@finbheara/archive-client 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
@@ -194,14 +194,19 @@ A release holds a feis-year once its results are captured and carries no calenda
194
194
  ```ts
195
195
  const a = await rel.schools.resolve("Ardvarna Academy of Irish Dance (Calgary, AB)", { region: "WCRO" });
196
196
  a.arm; // "unique" | "ambiguous" | "none"
197
- a.candidates; // [{ school_id, display, rows, by, prints, regions, places }]
197
+ a.candidates; // [{ school_id, display, rows, by, prints, regions, places, years? }]
198
198
  ```
199
199
 
200
200
  The rungs, the first that answers wins: a school id, a retired id followed to the live one, the
201
201
  print exactly as the release published it, then the print read by `@finbheara/domain`'s school
202
202
  reader at its filing key (`sn3`) and at its real words. `ambiguous` and `none` are answers: a
203
203
  best guess is never promoted to `unique`. `region` drops only a candidate the registry places in
204
- other regions, and keeps one it places nowhere. A print the reader refuses (a placeholder)
204
+ other regions, and keeps one it places nowhere. `year` reads a print at that year, on a print
205
+ rung only: where the print reaches two or more ids, the ids whose rows printed it that year are
206
+ kept (a print one school took over from another names the one that held it then), else the ids
207
+ that printed it last before that year, and where the year is before every one of them, all stay
208
+ (`a.year`: `{ asked, narrowed }`, each candidate's `years: { lo, hi }`). A dancer question reads
209
+ its school print at its own year. A print the reader refuses (a placeholder)
205
210
  answers `none` with the reason. An id (or a retired id followed) answers with every print its
206
211
  rows were published under and their rows, so it agrees with each print lookup of that school.
207
212
 
@@ -262,8 +267,8 @@ year with the asked age, or a dancer the release publishes in the other populati
262
267
  majors solo competitions and in none of the asked one's (listed in `excluded`, `why: "age"` or
263
268
  `"population"`, and a candidate contradicting both says `age`). A dancer published in both, or in
264
269
  neither, stays. A population word that names no one population (`mixed`) narrows nothing, and the
265
- answer says so (`r.population.note`). The school is evidence and order, never a reason
266
- to drop anyone: candidates whose latest school is the asked one, or one the lineage joins to it,
270
+ answer says so (`r.population.note`). Outside the narrowing below, the school is evidence and
271
+ order, never a reason to drop anyone: candidates whose latest school is the asked one, or one the lineage joins to it,
267
272
  come first, then those that ever danced for it, then the rest. A school row meets the asked school by its id, else by its print
268
273
  (one `rel.schools` resolves to the asked ids), so a latest print the release
269
274
  filed under no id is still `same`. `unique` is one candidate left after the age, never a best
@@ -297,8 +302,31 @@ words):
297
302
  one candidate is open and the age did not set it aside: the candidates the school refuted then
298
303
  move to `excluded` (`why: "school"`). Otherwise the arm stays `ambiguous` with every candidate,
299
304
  and where the school and the region single out no one, `preferred` names the only open candidate
300
- with a row in the asked year or the year before. A full given name is never narrowed this way: two
301
- full given names that differ are two names, and the school only orders them.
305
+ with a row in the asked year or the year before.
306
+
307
+ A FULL GIVEN NAME with a school the release resolves is narrowed the same way (`r.fullName`, the
308
+ same shape as `r.initials`), with one more hard fact:
309
+
310
+ - `given`: a candidate the name met only through an initial print (`Quellan, A.`) that also
311
+ printed a full given name the names package relates in no way to the asked one is another
312
+ dancer. An initial-only candidate, a variant or a diminutive (`nameKinship`) stays.
313
+
314
+ A candidate at another school is set aside only by the hard school fact, never for being
315
+ elsewhere: a dancer who ever danced for the asked school's line stays open.
316
+
317
+ THE WIDENED PASSES (`r.widened`), only where the name as asked is left with no one, so a widened
318
+ match never answers over an exact one:
319
+
320
+ - `birth-conflict`: a birth window the release flags (`birth.conflict`) is read one year wider on
321
+ each side, and the candidate says so (`candidate.widened`).
322
+ - `name-form`: the name read in its other forms by `@finbheara/names`: `composeNames` (a print
323
+ that left out a second surname, a middle name or a given name's letters, or a double-barrelled
324
+ surname read as its parts) or else `nameKinship` (a given name's variant or diminutive). Such a
325
+ candidate's `nameLevel` is `form` and `evidence.form` lists the tolerances. A form is set
326
+ aside only by the hard school fact (`why: "name-form"`, the fact in `r.fullName.refuted` or
327
+ `r.initials.refuted` where the answer was narrowed): every school row of the dancer, to the
328
+ asked year, reads outside the asked school's line. A dancer with no school row, or a print no
329
+ school reads, is unknown and stays.
302
330
 
303
331
  THE REGIONAL OIREACHTAS (`oireachtas: { region, year? }`). A dancer enters one
304
332
  residence-qualifying regional Oireachtas a year. Two regionals in one year are two dancers, the
@@ -77,7 +77,10 @@ import {
77
77
  populationRelation
78
78
  } from "@finbheara/domain/title";
79
79
  import {
80
+ composeNames,
80
81
  foldNameKey,
82
+ nameBlockKeys,
83
+ nameKinship,
81
84
  surnameBlock
82
85
  } from "@finbheara/names/normalize";
83
86
  import namesPkg from "@finbheara/names/package.json";
@@ -91,6 +94,7 @@ var NAME_LEVELS2 = [
91
94
  ...NAME_LEVEL_COLUMNS,
92
95
  "initial",
93
96
  "middle",
97
+ "form",
94
98
  "surname",
95
99
  "like"
96
100
  ];
@@ -640,7 +644,7 @@ var PERSON = { table: "person", value: "id", valueType: "INTEGER PRIMARY KEY" };
640
644
  var ASKED = ["surname_key", "nn4", "blk_surname"];
641
645
  var dancersIndex = {
642
646
  name: "dancers",
643
- version: "4",
647
+ version: "5",
644
648
  inputs: ["results"],
645
649
  keyParts: () => [...PACKAGE_KEY_PARTS],
646
650
  says: "every published dancer print at the names package's keys, its dancers, schools and years",
@@ -652,6 +656,8 @@ var dancersIndex = {
652
656
  out.run("CREATE TABLE dancer_school(dancer_id INTEGER NOT NULL, school_id TEXT, print TEXT NOT NULL, rows INTEGER NOT NULL, year_lo INTEGER, year_hi INTEGER)");
653
657
  out.run("CREATE TABLE dancer_population(dancer_id INTEGER NOT NULL, population TEXT NOT NULL, rows INTEGER NOT NULL, PRIMARY KEY(dancer_id, population)) WITHOUT ROWID");
654
658
  out.run("CREATE TABLE dancer_regional(dancer_id INTEGER NOT NULL, year INTEGER NOT NULL, region TEXT NOT NULL, regional INTEGER NOT NULL, PRIMARY KEY(dancer_id, year)) WITHOUT ROWID");
659
+ out.run("CREATE TABLE person_block(block TEXT NOT NULL, person INTEGER NOT NULL)");
660
+ const putBlock = out.prepare("INSERT INTO person_block VALUES (?, ?)");
655
661
  const src = rel.db("results");
656
662
  const groups = src.query(`SELECT r.name_printed AS p, r.name_order_resolved AS o, r.dancer_id AS d,
657
663
  r.school_printed AS sp, r.school_id AS s,
@@ -673,6 +679,8 @@ var dancersIndex = {
673
679
  id = ++next;
674
680
  names.add(name, id);
675
681
  personOf.set(reading, id);
682
+ for (const b of nameBlockKeys(name))
683
+ putBlock.run(b, id);
676
684
  }
677
685
  personOf.set(k, id);
678
686
  }
@@ -748,6 +756,7 @@ var dancersIndex = {
748
756
  for (const s of schema.indexes)
749
757
  if (ASKED.some((c) => s.endsWith(`(${c})`)))
750
758
  out.run(s);
759
+ out.run("CREATE INDEX person_block_block ON person_block(block)");
751
760
  out.run("CREATE INDEX print_dancer_person ON print_dancer(person)");
752
761
  out.run("CREATE INDEX print_dancer_dancer ON print_dancer(dancer_id)");
753
762
  out.run("CREATE INDEX dancer_school_dancer ON dancer_school(dancer_id)");
@@ -767,6 +776,7 @@ var NAME_WEIGHT = {
767
776
  nn4: 3,
768
777
  initial: 2,
769
778
  middle: 1.5,
779
+ form: 1.25,
770
780
  surname: 1,
771
781
  like: 1
772
782
  };
@@ -907,7 +917,56 @@ class DancerResolver2 {
907
917
  if (level !== null)
908
918
  hits.set(value, level);
909
919
  }
910
- return this.answer(db, q, hits);
920
+ const exact = await this.answer(db, q, hits);
921
+ if (exact.arm !== "none")
922
+ return exact;
923
+ const born = await this.answer(db, q, hits, true, { birthSlack: true });
924
+ if (born.arm !== "none")
925
+ return { ...born, widened: "birth-conflict" };
926
+ const forms = this.formsOf(db, names, asked, hits);
927
+ if (forms.size === 0)
928
+ return exact;
929
+ const all = new Map(hits);
930
+ for (const id of forms.keys())
931
+ all.set(id, "form");
932
+ const formed = await this.answer(db, q, all, true, { birthSlack: true, forms });
933
+ return formed.arm === "none" ? exact : { ...formed, widened: "name-form" };
934
+ }
935
+ formsOf(db, names, asked, hits) {
936
+ const out = new Map;
937
+ const keys = nameBlockKeys(asked);
938
+ const persons = new Set;
939
+ if (keys.length > 0)
940
+ for (const r of db.query(`SELECT DISTINCT person FROM person_block WHERE block IN (${keys.map(() => "?").join(",")})`).all(...keys))
941
+ persons.add(r.person);
942
+ const rows = new Map;
943
+ for (const { value, row } of names.rows({ surname_key: [asked.surnameKey()] }, [
944
+ "print",
945
+ "reading"
946
+ ]))
947
+ rows.set(value, row);
948
+ const ids = [...persons].filter((id) => !hits.has(id) && !rows.has(id));
949
+ for (let i = 0;i < ids.length; i += 500) {
950
+ const chunk = ids.slice(i, i + 500);
951
+ for (const r of db.query(`SELECT id, print, reading FROM person WHERE id IN (${chunk.map(() => "?").join(",")})`).all(...chunk))
952
+ rows.set(r.id, { print: r.print, reading: r.reading });
953
+ }
954
+ for (const [id, row] of rows) {
955
+ if (hits.has(id))
956
+ continue;
957
+ const c = ENCODER.decode(row);
958
+ if (!c.isPerson())
959
+ continue;
960
+ const composed = composeNames(asked, c);
961
+ if (composed.composition !== "conflict" && composed.composition !== "silent") {
962
+ out.set(id, [...composed.via]);
963
+ continue;
964
+ }
965
+ const kin = nameKinship(asked, c);
966
+ if (kin.relation !== "different")
967
+ out.set(id, [...kin.via]);
968
+ }
969
+ return out;
911
970
  }
912
971
  async search(q) {
913
972
  if (q.name !== undefined) {
@@ -958,7 +1017,7 @@ class DancerResolver2 {
958
1017
  schools: schoolsOf(db, dancerId)
959
1018
  };
960
1019
  }
961
- async answer(db, q, hits, narrow = true) {
1020
+ async answer(db, q, hits, narrow = true, widen = {}) {
962
1021
  const asOf = yearOf(q.asOf);
963
1022
  const age = q.age === undefined ? undefined : this.readAge(q.age, asOf);
964
1023
  const population = q.population === undefined ? undefined : readPopulation2(q.population);
@@ -967,7 +1026,7 @@ class DancerResolver2 {
967
1026
  let lineageIds = [];
968
1027
  let school;
969
1028
  if (q.school !== undefined) {
970
- const s = await this.rel.schools.resolve(q.school);
1029
+ const s = await this.rel.schools.resolve(q.school, { year: askedYear(q, asOf) });
971
1030
  schoolIds = s.candidates.map((c) => c.school_id);
972
1031
  const lineage = await this.lineage();
973
1032
  if (lineage === null)
@@ -987,9 +1046,11 @@ class DancerResolver2 {
987
1046
  unlinked.set(r.print, (unlinked.get(r.print) ?? 0) + r.n);
988
1047
  continue;
989
1048
  }
990
- const e = byDancer.get(r.d) ?? { level, prints: [] };
1049
+ const e = byDancer.get(r.d) ?? { level, prints: [], via: new Set };
991
1050
  e.level = finer(e.level, level);
992
1051
  e.prints.push({ print: r.print, rows: r.n, level });
1052
+ for (const v of widen.forms?.get(person) ?? [])
1053
+ e.via.add(v);
993
1054
  byDancer.set(r.d, e);
994
1055
  }
995
1056
  const dq = db.query("SELECT * FROM dancer WHERE dancer_id = ?");
@@ -1000,7 +1061,15 @@ class DancerResolver2 {
1000
1061
  continue;
1001
1062
  const schools = schoolsOf(db, id);
1002
1063
  const latest = latestSchool(schools, asOf);
1003
- const ageEv = ageEvidence(d, age?.age ?? null);
1064
+ let ageEv = ageEvidence(d, age?.age ?? null);
1065
+ let widenedBirth = false;
1066
+ if (widen.birthSlack && ageEv === "contradicts" && d.birth_conflict !== 0) {
1067
+ const wider = ageEvidence(d, age?.age ?? null, 1);
1068
+ if (wider !== "contradicts") {
1069
+ ageEv = wider;
1070
+ widenedBirth = true;
1071
+ }
1072
+ }
1004
1073
  const pop = populationOf(db, id);
1005
1074
  const popEv = populationEvidence(pop, population?.read ?? null);
1006
1075
  const schoolEv = schoolIds === null ? "unasked" : schoolIds.length === 0 ? "unresolved" : await this.schoolEvidence(schools, schoolIds, lineageIds, asOf);
@@ -1021,8 +1090,10 @@ class DancerResolver2 {
1021
1090
  age: ageEv,
1022
1091
  school: schoolEv,
1023
1092
  population: popEv,
1024
- region: regionEv
1093
+ region: regionEv,
1094
+ ...e.level === "form" ? { form: [...e.via] } : {}
1025
1095
  },
1096
+ ...widenedBirth ? { widened: "birth-conflict" } : {},
1026
1097
  prints: e.prints.sort((a, b) => b.rows - a.rows || (a.print < b.print ? -1 : 1)),
1027
1098
  rows: d.rows,
1028
1099
  year_lo: d.year_lo,
@@ -1049,21 +1120,54 @@ class DancerResolver2 {
1049
1120
  return false;
1050
1121
  });
1051
1122
  const year = askedYear(q, asOf);
1052
- let initials;
1053
- if (narrow && kept.length > 1 && schoolIds !== null && schoolIds.length > 0 && initialsOnly2(dancerName(q.name, q.order === "unknown" ? undefined : q.order))) {
1123
+ const band = age?.age?.birthYears() ?? null;
1124
+ const adultBy = adultBornBy(year);
1125
+ const adultAsked = adultBy !== null && adultCategory(q.age, age?.age ?? null);
1126
+ const returningOf = (c) => adultAsked && c.birth.hi !== null && c.birth.hi <= adultBy ? c.birth.lo : null;
1127
+ let line = null;
1128
+ if (schoolIds !== null && schoolIds.length > 0) {
1054
1129
  const lineage = await this.lineage();
1055
- const line = lineage === null ? new Set(schoolIds) : schoolLine(lineage, schoolIds, year);
1056
- const band = age?.age?.birthYears() ?? null;
1130
+ line = lineage === null ? new Set(schoolIds) : schoolLine(lineage, schoolIds, year);
1131
+ }
1132
+ const formFacts = new Map;
1133
+ if (narrow && widen.forms !== undefined && line !== null) {
1134
+ const next = [];
1135
+ for (const c of kept) {
1136
+ if (c.evidence.nameLevel === "form") {
1137
+ const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school, returningOf(c));
1138
+ if (fact !== null) {
1139
+ formFacts.set(c.dancer_id, fact);
1140
+ excluded.push({ candidate: c, why: "name-form" });
1141
+ continue;
1142
+ }
1143
+ }
1144
+ next.push(c);
1145
+ }
1146
+ kept = next;
1147
+ }
1148
+ const askedName = dancerName(q.name, q.order === "unknown" ? undefined : q.order);
1149
+ const byInitials = initialsOnly2(askedName);
1150
+ let narrowing;
1151
+ if (narrow && kept.length > 1 && line !== null) {
1057
1152
  const slack = q.age !== undefined && !("yob" in q.age) ? DANCE_UP_YEARS : 0;
1058
1153
  const open = [];
1059
1154
  const held = [];
1060
1155
  const refuted = [];
1061
1156
  const bySchool = new Set;
1062
- const adultBy = adultBornBy(year);
1063
- const adultAsked = adultBy !== null && adultCategory(q.age, age?.age ?? null);
1157
+ const givenFact = (c) => {
1158
+ if (byInitials || c.evidence.nameLevel !== "initial")
1159
+ return false;
1160
+ const fact = otherGiven(db, c.dancer_id, askedName);
1161
+ if (fact === null)
1162
+ return false;
1163
+ refuted.push({ dancer_id: c.dancer_id, why: "given", fact });
1164
+ bySchool.add(c.dancer_id);
1165
+ return true;
1166
+ };
1064
1167
  const schoolFact = async (c) => {
1065
- const returning = adultAsked && c.birth.hi !== null && c.birth.hi <= adultBy ? c.birth.lo : null;
1066
- const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school, returning);
1168
+ if (givenFact(c))
1169
+ return true;
1170
+ const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school, returningOf(c));
1067
1171
  if (fact === null)
1068
1172
  return false;
1069
1173
  refuted.push({ dancer_id: c.dancer_id, why: "school", fact });
@@ -1084,10 +1188,10 @@ class DancerResolver2 {
1084
1188
  refuted.push({
1085
1189
  dancer_id: c.dancer_id,
1086
1190
  why: x.why,
1087
- fact: contradictionFact(x, age, slack)
1191
+ fact: formFacts.get(c.dancer_id) ?? contradictionFact(x, age, slack)
1088
1192
  });
1089
1193
  }
1090
- initials = {
1194
+ narrowing = {
1091
1195
  open: [...open.map((c) => c.dancer_id), ...held],
1092
1196
  heldByDanceUp: held,
1093
1197
  refuted,
@@ -1096,7 +1200,10 @@ class DancerResolver2 {
1096
1200
  if (open.length === 1 && held.length === 0) {
1097
1201
  for (const c of kept)
1098
1202
  if (bySchool.has(c.dancer_id))
1099
- excluded.push({ candidate: c, why: "school" });
1203
+ excluded.push({
1204
+ candidate: c,
1205
+ why: refuted.find((r) => r.dancer_id === c.dancer_id)?.why === "given" ? "given" : "school"
1206
+ });
1100
1207
  kept = open;
1101
1208
  }
1102
1209
  }
@@ -1126,8 +1233,8 @@ class DancerResolver2 {
1126
1233
  };
1127
1234
  }
1128
1235
  }
1129
- if (preferred === undefined && kept.length > 1 && initials !== undefined) {
1130
- const openIds = new Set(initials.open);
1236
+ if (preferred === undefined && kept.length > 1 && narrowing !== undefined) {
1237
+ const openIds = new Set(narrowing.open);
1131
1238
  const hit = kept.filter((c) => openIds.has(c.dancer_id) && c.year_hi !== null && c.year_hi >= year - 1);
1132
1239
  if (hit.length === 1) {
1133
1240
  const c = hit[0];
@@ -1149,7 +1256,7 @@ class DancerResolver2 {
1149
1256
  ...population !== undefined ? { population } : {},
1150
1257
  ...oireachtas !== undefined ? { oireachtas } : {},
1151
1258
  ...school !== undefined ? { school } : {},
1152
- ...initials !== undefined ? { initials } : {}
1259
+ ...narrowing !== undefined ? byInitials ? { initials: narrowing } : { fullName: narrowing } : {}
1153
1260
  };
1154
1261
  }
1155
1262
  async readOireachtas(db, o, asOf) {
@@ -1282,6 +1389,22 @@ function youthRow(s, bornLo) {
1282
1389
  const by = adultBornBy(s.year_hi);
1283
1390
  return by !== null && bornLo > by;
1284
1391
  }
1392
+ function otherGiven(db, dancerId, asked) {
1393
+ const r = db.query("SELECT names_printed FROM dancer WHERE dancer_id = ?").get(dancerId);
1394
+ if (r === null)
1395
+ return null;
1396
+ for (const print of JSON.parse(r.names_printed)) {
1397
+ const p = dancerName(print);
1398
+ if (!p.isPerson() || (p.givenFields()[0]?.length ?? 0) < 2)
1399
+ continue;
1400
+ if (nameLevel2(asked, p) !== null)
1401
+ continue;
1402
+ if (nameKinship(asked, p).relation !== "different")
1403
+ continue;
1404
+ return `printed "${print}", another given name than the asked one, and met it only through an initial`;
1405
+ }
1406
+ return null;
1407
+ }
1285
1408
  function birthOf(d) {
1286
1409
  return {
1287
1410
  lo: d.birth_lo,
@@ -1349,12 +1472,12 @@ function regionEvidence(db, dancerId, region, year) {
1349
1472
  return { word: "contradicts", region: at(latest) };
1350
1473
  return { word: latest.region === region ? "same" : "moved", region: at(latest) };
1351
1474
  }
1352
- function ageEvidence(d, asked) {
1475
+ function ageEvidence(d, asked, slack = 0) {
1353
1476
  if (asked === null)
1354
1477
  return "unasked";
1355
1478
  if (d.birth_lo === null && d.birth_hi === null)
1356
1479
  return "unknown";
1357
- const own = Age.fromYOB(d.birth_lo, d.birth_hi);
1480
+ const own = Age.fromYOB(d.birth_lo === null ? null : d.birth_lo - slack, d.birth_hi === null ? null : d.birth_hi + slack);
1358
1481
  if (!own.isAge())
1359
1482
  return "unknown";
1360
1483
  if (own.fitsIn(asked))
@@ -1989,22 +2112,28 @@ var encoderOf = (r) => schoolEncoder({ reader: r.reader });
1989
2112
  var TABLE = "school_name";
1990
2113
  var schoolsIndex = {
1991
2114
  name: "schools",
1992
- version: "2",
2115
+ version: "3",
1993
2116
  inputs: ["results", "school-registry.jsonl", "school-redirects.jsonl"],
1994
2117
  says: "every published school print and its ids, read at the school reader's slots",
1995
2118
  keyParts: (rel) => [`reader=${rel.schoolReader().id}`],
1996
2119
  build(rel, out) {
1997
2120
  const choice = rel.schoolReader();
1998
2121
  const encoder = encoderOf(choice);
1999
- out.run("CREATE TABLE print_id(print TEXT NOT NULL, school_id TEXT NOT NULL, rows INTEGER NOT NULL, PRIMARY KEY(print, school_id)) WITHOUT ROWID");
2122
+ out.run("CREATE TABLE print_id(print TEXT NOT NULL, school_id TEXT NOT NULL, rows INTEGER NOT NULL, year_lo INTEGER, year_hi INTEGER, PRIMARY KEY(print, school_id)) WITHOUT ROWID");
2000
2123
  out.run("CREATE TABLE registry(school_id TEXT PRIMARY KEY, display TEXT NOT NULL, regions TEXT NOT NULL, places TEXT NOT NULL)");
2001
2124
  out.run("CREATE TABLE redirect(old_id TEXT PRIMARY KEY, new_id TEXT NOT NULL, kind TEXT NOT NULL)");
2002
2125
  const schema = indexSchema3(encoder, { table: TABLE });
2003
2126
  out.run(schema.table);
2004
- const pairs = rel.db("results").query("SELECT trim(school_printed) AS p, school_id AS s, count(*) AS n FROM results WHERE school_id IS NOT NULL GROUP BY 1, 2 ORDER BY 1, 2").all();
2005
- const putPair = out.prepare("INSERT INTO print_id VALUES (?, ?, ?)");
2127
+ const src = rel.db("results");
2128
+ const lined = src.query("PRAGMA table_info(results)").all().some((c) => c.name === "competition_line");
2129
+ const pairs = src.query(lined ? `SELECT trim(r.school_printed) AS p, r.school_id AS s, count(*) AS n,
2130
+ min(c.year) AS lo, max(c.year) AS hi
2131
+ FROM results r LEFT JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
2132
+ WHERE r.school_id IS NOT NULL GROUP BY 1, 2 ORDER BY 1, 2` : `SELECT trim(school_printed) AS p, school_id AS s, count(*) AS n, NULL AS lo, NULL AS hi
2133
+ FROM results WHERE school_id IS NOT NULL GROUP BY 1, 2 ORDER BY 1, 2`).all();
2134
+ const putPair = out.prepare("INSERT INTO print_id VALUES (?, ?, ?, ?, ?)");
2006
2135
  for (const r of pairs)
2007
- putPair.run(r.p, r.s, r.n);
2136
+ putPair.run(r.p, r.s, r.n, r.lo, r.hi);
2008
2137
  out.run("CREATE INDEX print_id_school ON print_id(school_id)");
2009
2138
  const names = createIndex3(out, encoder, {
2010
2139
  table: TABLE
@@ -2049,6 +2178,23 @@ class SchoolResolver2 {
2049
2178
  };
2050
2179
  const answer = (candidates, extra = {}) => {
2051
2180
  let cs = candidates.sort((a, b) => b.rows - a.rows || (a.school_id < b.school_id ? -1 : 1));
2181
+ let year;
2182
+ if (q.year !== undefined && cs.length > 1 && cs.every((c) => c.years !== undefined)) {
2183
+ const y = q.year;
2184
+ const span = (c) => c.years;
2185
+ let kept = cs.filter((c) => {
2186
+ const { lo, hi } = span(c);
2187
+ return lo !== null && hi !== null && lo <= y && y <= hi;
2188
+ });
2189
+ if (kept.length === 0) {
2190
+ const before = cs.filter((c) => span(c).hi !== null && span(c).hi < y);
2191
+ const last = Math.max(...before.map((c) => span(c).hi));
2192
+ kept = before.filter((c) => span(c).hi === last);
2193
+ }
2194
+ year = { asked: y, narrowed: kept.length > 0 && kept.length < cs.length };
2195
+ if (kept.length > 0)
2196
+ cs = kept;
2197
+ }
2052
2198
  let region;
2053
2199
  if (q.region !== undefined) {
2054
2200
  const kept = cs.filter((c) => c.regions.length === 0 || c.regions.includes(q.region));
@@ -2062,6 +2208,7 @@ class SchoolResolver2 {
2062
2208
  arm: cs.length === 0 ? "none" : cs.length === 1 ? "unique" : "ambiguous",
2063
2209
  candidates: cs,
2064
2210
  ...region !== undefined ? { region } : {},
2211
+ ...year !== undefined ? { year } : {},
2065
2212
  ...extra
2066
2213
  };
2067
2214
  };
@@ -2085,15 +2232,27 @@ class SchoolResolver2 {
2085
2232
  return answer([byId(live, "redirect")], { redirected: { from: a, to: live } });
2086
2233
  const byPrints = (prints, by) => {
2087
2234
  const ids = new Map;
2088
- const q2 = db.query("SELECT school_id, rows FROM print_id WHERE print = ?");
2235
+ const q2 = db.query("SELECT school_id, rows, year_lo AS lo, year_hi AS hi FROM print_id WHERE print = ?");
2089
2236
  for (const p of prints)
2090
2237
  for (const r of q2.all(p)) {
2091
- const e = ids.get(r.school_id) ?? { rows: 0, prints: new Set };
2238
+ const e = ids.get(r.school_id) ?? {
2239
+ rows: 0,
2240
+ prints: new Set,
2241
+ lo: null,
2242
+ hi: null
2243
+ };
2092
2244
  e.rows += r.rows;
2093
2245
  e.prints.add(p);
2246
+ if (r.lo !== null && (e.lo === null || r.lo < e.lo))
2247
+ e.lo = r.lo;
2248
+ if (r.hi !== null && (e.hi === null || r.hi > e.hi))
2249
+ e.hi = r.hi;
2094
2250
  ids.set(r.school_id, e);
2095
2251
  }
2096
- return [...ids].map(([id, e]) => candidate(id, by, e.rows, [...e.prints].sort()));
2252
+ return [...ids].map(([id, e]) => ({
2253
+ ...candidate(id, by, e.rows, [...e.prints].sort()),
2254
+ years: { lo: e.lo, hi: e.hi }
2255
+ }));
2097
2256
  };
2098
2257
  const exact = byPrints([a], "print");
2099
2258
  if (exact.length > 0)
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  nameOrderOf,
5
5
  openRelease2,
6
6
  releases2
7
- } from "./chunks/index-xtv455ft.js";
7
+ } from "./chunks/index-1ty89s8d.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ import {
46
46
  openCached2,
47
47
  ReleaseManager2,
48
48
  releases2
49
- } from "./chunks/index-xtv455ft.js";
49
+ } from "./chunks/index-1ty89s8d.js";
50
50
  import {
51
51
  cacheRoot2,
52
52
  repoOf2
@@ -1,5 +1,5 @@
1
1
  import { type PersonName, type TransitiveLevel } from "@finbheara/names/normalize";
2
- export type NameLevel = TransitiveLevel | "initial" | "middle" | "surname" | "like";
2
+ export type NameLevel = TransitiveLevel | "initial" | "middle" | "form" | "surname" | "like";
3
3
  export declare const NAME_LEVELS: readonly NameLevel[];
4
4
  export declare const finer: (a: NameLevel, b: NameLevel) => NameLevel;
5
5
  export declare function nameLevel(q: PersonName, c: PersonName): NameLevel | null;
@@ -52,7 +52,9 @@ export interface DancerCandidate {
52
52
  school: SchoolEvidence;
53
53
  population: PopulationEvidence;
54
54
  region: RegionEvidence;
55
+ form?: string[];
55
56
  };
57
+ widened?: "birth-conflict";
56
58
  prints: {
57
59
  print: string;
58
60
  rows: number;
@@ -82,13 +84,19 @@ export interface DancerRegion {
82
84
  }
83
85
  export interface ExcludedCandidate {
84
86
  candidate: DancerCandidate;
85
- why: "age" | "population" | "region" | "school";
87
+ why: "age" | "population" | "region" | "school" | "given" | "name-form";
86
88
  }
87
89
  export interface InitialsRefuted {
88
90
  dancer_id: number;
89
- why: "school" | "age" | "population" | "region";
91
+ why: "school" | "age" | "population" | "region" | "given" | "name-form";
90
92
  fact: string;
91
93
  }
94
+ export interface Narrowing {
95
+ open: number[];
96
+ heldByDanceUp: number[];
97
+ refuted: InitialsRefuted[];
98
+ year: number;
99
+ }
92
100
  export interface DancerAnswer {
93
101
  tag: string;
94
102
  asked: DancerQuery;
@@ -128,12 +136,9 @@ export interface DancerAnswer {
128
136
  lineage?: string[];
129
137
  };
130
138
  refused?: string;
131
- initials?: {
132
- open: number[];
133
- heldByDanceUp: number[];
134
- refuted: InitialsRefuted[];
135
- year: number;
136
- };
139
+ initials?: Narrowing;
140
+ fullName?: Narrowing;
141
+ widened?: "birth-conflict" | "name-form";
137
142
  }
138
143
  export interface DancerSearchQuery extends Omit<DancerQuery, "name"> {
139
144
  name?: string;
@@ -216,6 +221,7 @@ export declare class DancerResolver {
216
221
  years: DancerYear[];
217
222
  }>;
218
223
  resolve(q: DancerQuery): Promise<DancerAnswer>;
224
+ private formsOf;
219
225
  search(q: DancerSearchQuery): Promise<DancerAnswer>;
220
226
  private capped;
221
227
  get(dancerId: number): Promise<DancerRecord | null>;
@@ -1,6 +1,6 @@
1
1
  export { type AssetPlan, DATABASES, type DatabaseName, type LandedFile, type Receipt, type ReleaseManifest, planAsset, } from "./assets.js";
2
2
  export { sha256File, unzstdTo } from "./bytes.js";
3
- export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerBulkRecord, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerRegion, type DancerSchool, type DancerSearchQuery, type DancerYear, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type InitialsRefuted, initialsOnly, type OireachtasAsked, type PopulationEvidence, type RegionEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
3
+ export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerBulkRecord, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerRegion, type DancerSchool, type DancerSearchQuery, type DancerYear, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type InitialsRefuted, type Narrowing, initialsOnly, type OireachtasAsked, type PopulationEvidence, type RegionEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
4
4
  export { NAME_LEVELS, type NameLevel, nameLevel } from "./dancers-names.js";
5
5
  export { type Comp, type CompAnswer, type CompDance, type CompKind, type CompQuery, CompResolver, type CompSource, type FeisPlace, type SyllabusAnswer, type SyllabusForAnswer, type SyllabusSource, type TitleCheck, canonicalOf, } from "./comps.js";
6
6
  export { type ClientOptions, cacheRoot, repoOf } from "./config.js";
@@ -10,6 +10,10 @@ export interface SchoolCandidate {
10
10
  prints: string[];
11
11
  regions: string[];
12
12
  places: string[];
13
+ years?: {
14
+ lo: number | null;
15
+ hi: number | null;
16
+ };
13
17
  }
14
18
  export interface SchoolAnswer {
15
19
  tag: string;
@@ -20,6 +24,10 @@ export interface SchoolAnswer {
20
24
  asked: string;
21
25
  narrowed: boolean;
22
26
  };
27
+ year?: {
28
+ asked: number;
29
+ narrowed: boolean;
30
+ };
23
31
  redirected?: {
24
32
  from: string;
25
33
  to: string;
@@ -28,6 +36,7 @@ export interface SchoolAnswer {
28
36
  }
29
37
  export interface SchoolQuery {
30
38
  region?: string;
39
+ year?: number;
31
40
  }
32
41
  export interface SchoolReaderChoice {
33
42
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Open a versioned release of an Irish dance results archive: download, verify, decompress and cache it by tag, keep the cache, and resolve feises and schools against it",
5
5
  "keywords": ["irish-dance", "results", "release", "cache", "sqlite"],
6
6
  "license": "MIT",