@finbheara/domain 0.13.1 → 0.19.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
@@ -262,6 +262,7 @@ Age.fromTitle("Open Championship 15-16", { year: 2026 }).token; // "15-
262
262
  Age.fromTitle("Competition 719OC - Open Championship (O18)").token; // "o18", not "o9"
263
263
  Age.fromTitle("Treble Reel Special", { year: 2026 }).age.isEmpty(); // true
264
264
  Age.fromTitle("Mixed Reel 7 & Under").token; // "u8": a 7-year-old dances in it
265
+ Age.fromTitle("Mixed Reel Under 08 / 2 yr").token; // "u8/2yr": the two ages under 8
265
266
  Age.fromTitle("Girls 8 and under 9 years").token; // "8-9"
266
267
  Age.fromTitle("Junior Boys 12 Years").token; // "u13"
267
268
  Age.fromTitle("10-11 Single Jig").token; // "10-11", a band, not a number
@@ -271,7 +272,7 @@ Age.fromTitle("Beginner Adults Reel U40").token; // "adult u40": the adul
271
272
  Age.fromTitle("Solo Championship - Adult O40").token; // "adult o40": adult, and 40 or over
272
273
  Age.fromTitle("Adult Championship 18 - 29").token; // "adult 18-29": adult, and the range
273
274
  Age.fromTitle("Adult / Child Reel Under 10").token; // "u10": no adult dances in it
274
- Age.fromTitle("Senior Adult Ladies 19 & Over").token; // "adult o19": "Senior Adult" is adult
275
+ Age.fromTitle("Senior Adult Ladies 19 & Over").token; // "o19": TIDA's oldest youth championship
275
276
  Age.fromTitle("Senior Adult Men 17 & Over").token; // "o17": it admits under-18s
276
277
  Age.fromTitle("Championship (Adult) (A40U)").token; // "adult u40": a split adult code
277
278
  Age.fromTitle("Traditional Set (A40O)").token; // "adult o40"
@@ -288,10 +289,10 @@ An adult word beside an and-over from 18 or a range from 18 reads both as well:
288
289
  `adult 18-29` are adult (`isAdult()`), and their `kind`, interval and birth years are the ones the
289
290
  band alone reads on the same ladder, a range floored at 18 where no rung sits below it. So
290
291
  `Age.from("adult o40", { year: 2026 })` is born 1985 or earlier, as `o40` is. An and-over under
291
- 18 or a range opening under 18 (`Teen & Adult 13 & Over`) admits youth, and the band stands. A
292
- title naming the senior word beside it reads the same: `Senior Adult` is the adult band, so
293
- `Senior Adult Ladies 19 & Over` is `adult o19`. A closing split adult code keeps its split:
294
- `(A40U)` is `adult u40`, `(A40O)` and `(A40&O)` are `adult o40`.
292
+ 18 or a range opening under 18 (`Teen & Adult 13 & Over`) admits youth, and the band stands. TIDA's
293
+ `Senior Adult` beside an and-over is its oldest youth championship, an open championship that
294
+ qualifies for the Worlds, so `Senior Adult Ladies 19 & Over` is `o19`. A closing split
295
+ adult code keeps its split: `(A40U)` is `adult u40`, `(A40O)` and `(A40&O)` are `adult o40`.
295
296
 
296
297
  `adultBeside(band)` is that reading as one function, the band beside the adult word as one token,
297
298
  or `undefined` where the band admits youth. A caller holding a band from another field asks it: a
@@ -300,6 +301,12 @@ platform's printed `18&O` beside a title that reads adult is `adult o18`.
300
301
  An age group is the age on the first of January, so a title that admits the dancer who is
301
302
  `N` (`7 & Under`, `7/U`, `7 Years`) is the `Under N+1` group.
302
303
 
304
+ An under band may print its stride, the number of ages it holds: `Under 08 / 2 yr` is the ages 6
305
+ and 7, and reads `u8/2yr`. `Age.from("u8/2yr")` is `[6, 8)` whatever the ladder offers, since the
306
+ page states the floor (`clauses` is `["under-stride"]`), where a plain `u8` takes the rung below
307
+ it. So `Age.from("u8/2yr", { year: 2026 })` is born 2018 or 2019. Beside the adult word the stride
308
+ never opens below 18: `adult u40/10yr` is `[30, 40)`.
309
+
303
310
  A value the reader cannot stand behind is returned, not thrown. An `Age` is `Validated` (above):
304
311
  an age group under `U3` admits nobody old enough to dance, and a year of birth outside 1901 to
305
312
  2099 is not one, so the placeholders `1000` and `1900` are refused. An invalid `Age` has no
package/dist/age.js CHANGED
@@ -4,14 +4,17 @@ import {
4
4
 
5
5
  var LEAD_ID = /^\s*(?:comp(?:etition)?\.?\s*)?\d+[a-z]*(?:[/-]\d*[a-z]*)*\s*[-:.]?\s*/i;
6
6
  var PAREN = /\(([^)]*)\)/g;
7
- var AND_OVER = /(?<![a-z0-9])(\d{1,2})\s*(?:years?\s*)?(?:&|\+|\band\b)\s*(?:over|older|up|o)(?![a-z])/i;
8
- var OVER_WORD = /(?:\bover\s*|\bos\s+cionn\s*|(?<![a-z0-9])o\.?\s*)(\d{1,2})(?![a-z0-9])/i;
7
+ var AND_OVER = /(?<![0-9])(?<![0-9][a-z]*)(?<!(?:^|[^a-z0-9])[a-z])(\d{1,2})\s*(?:years?\s*)?(?:&|\+|\band\b)\s*(?:over|older|up|o)(?![a-z])/i;
8
+ var OVER_WORD = /(?:\bover\s*|\bos\s+cionn\s*|(?<![a-z0-9])o[./]?\s*)(\d{1,2})(?![a-z0-9])/i;
9
9
  var OVER_GLUED = /(?<![a-z0-9])(\d{1,2})\s*o(?![a-z'’])/i;
10
10
  var OVER_PLUS = /(?<![a-z0-9])(\d{1,2})\s*\+/;
11
- var UNDER = /(?:\bunder\s*|\bfaoi\s*|(?<![a-z0-9])u\.?\s*)(\d{1,2})(?![a-z0-9])/i;
11
+ var UNDER = /(?:\bunder\s*(?:age\s*)?|\bfaoi\s*|(?<![a-z0-9])u\.?\s*)(\d{1,2})(?![0-9])(?![a-z][a-z0-9])/i;
12
+ var UNDER_STRIDE = /^\s*\/\s*0*([1-9]\d?)\s*yrs?(?![a-z0-9])/i;
12
13
  var AND_UNDER = /(?<![a-z0-9])(\d{1,2})\s*(?:years?\s*)?(?:&|\/|\band\b)\s*u(?:nder)?(?![a-z])(?!\s*\d)/i;
13
14
  var AND_UNDER_RANGE = /(?<![a-z0-9])(\d{1,2})\s*(?:&|\band\b)\s*under\s*(\d{1,2})(?![a-z0-9])/i;
14
15
  var AND_RANGE = /(?<![a-z0-9])(?<!\b(?:under|und|over|faoi|cionn|u|o)\.?\s*)(?<!\d\s*,\s*)(?<!\(\s*)(\d{1,2})\s*(?:&|\band\b)\s*(\d{1,2})(?![a-z0-9])(?!\s*-?\s*hand)(?!\s*(?:&|\+|\/|,|\)|\band\b|\bor\b))/i;
16
+ var SLASH_RANGE = /(?<![a-z0-9/])(\d{1,2})\s*\/\s*(\d{1,2})(?=\s*(?:years?|yrs?|boys?|girls?|ladies|men)\b)/i;
17
+ var AGE_N = /(?<!\b(?:under|over)\s*)\bage\s*(\d{1,2})(?![a-z0-9])(?!\s*-?\s*hand)/i;
15
18
  var YEARS = /(?<![a-z0-9])(?<!\d\s*(?:&|\/|\band\b|[-\u0096–—]|\bto\b)\s*)(\d{1,2})\s*(?:years?|yrs?)(?![a-z0-9])(?!\s*(?:&|\+|\/|\band\b|\bor\b|old))/i;
16
19
  var RANGE_DASH = String.raw`(?:\u00c2?[-\u0096–—]|\bto\b)`;
17
20
  var RANGE = new RegExp(String.raw`(?<![0-9])(?<![0-9][a-z]*)(?<!(?:^|[^a-z0-9])[a-z])(?:u\s*)?(\d{1,2})\s*${RANGE_DASH}\s*(\d{1,2})(?![a-z0-9])`, "i");
@@ -28,6 +31,8 @@ function at(title, offset, start, end, token) {
28
31
  return { token, span: { start: a, end: b, text: title.slice(a, b) } };
29
32
  }
30
33
  function inFence(a, b) {
34
+ if (a.startsWith("0") || b.startsWith("0"))
35
+ return false;
31
36
  const lo = Number(a);
32
37
  const hi = Number(b);
33
38
  return lo >= TITLE_RANGE_MIN && hi <= TITLE_RANGE_MAX && lo < hi;
@@ -44,6 +49,9 @@ function readAnchored(title, s, offset, aside = false) {
44
49
  const j = aside ? null : AND_RANGE.exec(s);
45
50
  if (j?.[1] !== undefined && j[2] !== undefined && inFence(j[1], j[2]))
46
51
  return at(title, offset, j.index, j.index + j[0].length, `${Number(j[1])}-${Number(j[2])}`);
52
+ const sl = aside ? null : SLASH_RANGE.exec(s);
53
+ if (sl?.[1] !== undefined && sl[2] !== undefined && inFence(sl[1], sl[2]) && Number(sl[2]) === Number(sl[1]) + 1)
54
+ return at(title, offset, sl.index, sl.index + sl[0].length, `${Number(sl[1])}-${Number(sl[2])}`);
47
55
  const l = LEAD_RANGE.exec(s);
48
56
  if (l?.[1] !== undefined && l[2] !== undefined && inFence(l[1], l[2])) {
49
57
  const lead = l[0].length - l[0].trimStart().length;
@@ -58,9 +66,14 @@ function readAnchored(title, s, offset, aside = false) {
58
66
  if (au?.[1] !== undefined)
59
67
  return at(title, offset, au.index, au.index + au[0].length, `u${Number(au[1]) + 1}`);
60
68
  const m = UNDER.exec(s);
61
- if (m?.[1] !== undefined)
62
- return at(title, offset, m.index, m.index + m[0].length, `u${Number(m[1])}`);
63
- const y = YEARS.exec(s);
69
+ if (m?.[1] !== undefined) {
70
+ const end = m.index + m[0].length;
71
+ const k = UNDER_STRIDE.exec(s.slice(end));
72
+ if (k?.[1] !== undefined)
73
+ return at(title, offset, m.index, end + k[0].length, `u${Number(m[1])}/${Number(k[1])}yr`);
74
+ return at(title, offset, m.index, end, `u${Number(m[1])}`);
75
+ }
76
+ const y = YEARS.exec(s) ?? AGE_N.exec(s);
64
77
  if (y?.[1] !== undefined)
65
78
  return at(title, offset, y.index, y.index + y[0].length, `u${Number(y[1]) + 1}`);
66
79
  const p = OVER_PLUS.exec(s);
@@ -111,15 +124,17 @@ function ageTitleToken(title) {
111
124
  return adultAside !== null && (words.token === "" || words.token === "adult") ? adultAside : adultOverUnder(title, s, lead, words);
112
125
  }
113
126
  var ADULT_UNDER_MIN = 18;
127
+ var SENIOR_ADULT_OVER = /\bsenior\s+adult\b/i;
114
128
  function adultBeside(band) {
115
129
  const b = band.trim().toLowerCase();
116
130
  if (b === "")
117
131
  return "adult";
118
- const n = /^u(\d+)$/.exec(b)?.[1];
132
+ const u = /^u(\d+)(\/\d+yr)?$/.exec(b);
133
+ const n = u?.[1];
119
134
  if (n !== undefined) {
120
135
  if (Number(n) < ADULT_UNDER_MIN)
121
136
  return;
122
- return Number(n) > ADULT_UNDER_MIN ? `adult u${Number(n)}` : "adult";
137
+ return Number(n) > ADULT_UNDER_MIN ? `adult u${Number(n)}${u?.[2] ?? ""}` : "adult";
123
138
  }
124
139
  const floor = /^o(\d+)$/.exec(b)?.[1] ?? /^(\d+)-\d+$/.exec(b)?.[1];
125
140
  if (floor === undefined)
@@ -132,10 +147,12 @@ function adultOverUnder(title, s, lead, t) {
132
147
  const a = ADULT.exec(s);
133
148
  if (a === null)
134
149
  return t;
150
+ if (/^o\d+$/.test(t.token) && SENIOR_ADULT_OVER.test(s))
151
+ return t;
135
152
  const token = adultBeside(t.token);
136
153
  if (token === undefined || token === t.token)
137
154
  return t;
138
- if (/^u\d+$/.test(t.token))
155
+ if (/^u\d+(?:\/\d+yr)?$/.test(t.token))
139
156
  return at(title, lead, a.index, a.index + a[0].length, token);
140
157
  return { token, span: t.span };
141
158
  }
@@ -153,12 +170,12 @@ var YOB_RANGE = Object.freeze({ first: 1901, last: 2099 });
153
170
  var DANCE_UP_YEARS = 1;
154
171
  var ageOnJanuary1 = (year, born) => year - born - 1;
155
172
  var yearsSinceBirth = (year, born) => year - born;
156
- var UNDER2 = /^(?:u|under\s*)(\d+)$/i;
173
+ var UNDER2 = /^(?:u|under\s*)(\d+)(?:\/([1-9]\d*)yr)?$/i;
157
174
  var OVER = /^(?:o|over\s*)(\d+)$/i;
158
175
  var AND_OVER2 = /^(\d+)\s*(?:&|and\b)\s*(?:over|o)$/i;
159
176
  var RANGE2 = /^u?(\d+)\s*[-–]\s*(\d+)$/i;
160
177
  var ADULT_WORD = /^(?:adults?|ad|advanced|masters?)$/i;
161
- var ADULT_UNDER = /^(?:adults?|ad)[\s-]*(?:u|under\s*)(\d+)$/i;
178
+ var ADULT_UNDER = /^(?:adults?|ad)[\s-]*(?:u|under\s*)(\d+)(?:\/([1-9]\d*)yr)?$/i;
162
179
  var ADULT_BAND = /^(?:adults?|ad)[\s-]+(\S.*)$/i;
163
180
  var SENIOR_WORD = /^seniors?$/i;
164
181
  var NO_LADDER = Object.freeze({ caps: [], overs: [], ranges: [] });
@@ -213,19 +230,28 @@ function readRange(a, b, ladder) {
213
230
  parts: [a, b]
214
231
  };
215
232
  }
233
+ function strideOf(k) {
234
+ if (k === undefined)
235
+ return null;
236
+ const n = Number(k);
237
+ return Number.isInteger(n) && n >= 1 ? n : null;
238
+ }
216
239
  function read(token, ladder, top) {
217
240
  const r = RANGE2.exec(token);
218
241
  if (r !== null)
219
242
  return readRange(Number(r[1]), Number(r[2]), ladder);
220
243
  const au = ADULT_UNDER.exec(token);
221
- if (au !== null && Number(au[1]) > ADULT_FLOOR)
244
+ if (au !== null && Number(au[1]) > ADULT_FLOOR) {
245
+ const k = strideOf(au[2]);
246
+ const hi = Number(au[1]);
222
247
  return {
223
248
  kind: "adult",
224
- interval: { lo: ADULT_FLOOR, hi: Number(au[1]) },
249
+ interval: { lo: k === null ? ADULT_FLOOR : Math.max(ADULT_FLOOR, hi - k), hi },
225
250
  refusal: null,
226
- clauses: ["adult-under-ceiling"],
227
- parts: [Number(au[1])]
251
+ clauses: k === null ? ["adult-under-ceiling"] : ["adult-under-ceiling", "under-stride"],
252
+ parts: k === null ? [hi] : [hi, k]
228
253
  };
254
+ }
229
255
  const ab = au === null ? ADULT_BAND.exec(token) : null;
230
256
  if (ab !== null) {
231
257
  const inner = read(ab[1].trim(), { ...ladder, adult: true }, top);
@@ -253,6 +279,15 @@ function read(token, ladder, top) {
253
279
  const u = UNDER2.exec(token);
254
280
  if (u !== null) {
255
281
  const n = Number(u[1]);
282
+ const k = strideOf(u[2]);
283
+ if (k !== null)
284
+ return {
285
+ kind: "under",
286
+ interval: { lo: Math.max(YOUNGEST_FLOOR, n - k), hi: n },
287
+ refusal: null,
288
+ clauses: ["under-stride"],
289
+ parts: [n, k]
290
+ };
256
291
  const below = maxWhere(ladder.caps, (c) => c < n);
257
292
  return {
258
293
  kind: "under",
@@ -357,7 +392,7 @@ function fromPrint(print, context = {}) {
357
392
  throw new Error(`Age.from(${JSON.stringify(print)}): birthYears needs a competition year`);
358
393
  return birthYearsOfInterval(iv, at);
359
394
  },
360
- floorObserved: () => reading.kind !== "under" && reading.kind !== "range" || iv === null ? null : reading.clauses.includes("under-floored-on-rung") || reading.clauses.includes("range-adult-floor")
395
+ floorObserved: () => reading.kind !== "under" && reading.kind !== "range" || iv === null ? null : reading.clauses.includes("under-floored-on-rung") || reading.clauses.includes("range-adult-floor") || reading.clauses.includes("under-stride")
361
396
  });
362
397
  }
363
398
  function fromYOB(from, to = from, context = {}) {
@@ -412,6 +447,8 @@ function sourceOf(r) {
412
447
  switch (r.kind) {
413
448
  case "range":
414
449
  case "under":
450
+ if (r.clauses.includes("under-stride"))
451
+ return "printed-band";
415
452
  return iv.lo > YOUNGEST_FLOOR ? "sibling-narrowed" : "printed-band";
416
453
  case "over":
417
454
  return iv.hi === Number.POSITIVE_INFINITY ? "printed-band" : "sibling-narrowed";
@@ -95,7 +95,7 @@ var PLACE_NODES = [
95
95
  name: "Cúige Chonnacht / Connacht",
96
96
  geo: "IRL",
97
97
  admin: "world",
98
- prints: ["chonnacht", "connacht", "connaght", "connaught", "cuige chonnacht"],
98
+ prints: ["chonnacht", "connacht", "connaght", "connaught", "cuige chonnacht", "cuige connacht"],
99
99
  codes: ["CONNACHT"],
100
100
  oireachtas: "CONNACHT"
101
101
  },
@@ -381,7 +381,7 @@ var PLACE_NODES = [
381
381
  name: "Cúige Uladh / Ulster",
382
382
  geo: "IRL",
383
383
  admin: "world",
384
- prints: ["cuige uladh", "cuige uldah", "uladh", "ulster"],
384
+ prints: ["cuige uladh", "cuige uldah", "cuige ulster", "uladh", "ulster"],
385
385
  codes: ["ULSTER"],
386
386
  oireachtas: "ULSTER"
387
387
  },
@@ -3575,6 +3575,276 @@ var PLACE_NODES = [
3575
3575
  prints: ["warwickshire"],
3576
3576
  codes: ["GB-WAR", "GBR-WAR"]
3577
3577
  },
3578
+ {
3579
+ id: "GBR-BDF",
3580
+ kind: "subdivision",
3581
+ name: "Bedfordshire",
3582
+ geo: "GBR-ENG",
3583
+ admin: null,
3584
+ prints: ["bedfordshire"],
3585
+ codes: ["GB-BDF", "GBR-BDF"],
3586
+ listedCutsOnly: true
3587
+ },
3588
+ {
3589
+ id: "GBR-BKM",
3590
+ kind: "subdivision",
3591
+ name: "Buckinghamshire",
3592
+ geo: "GBR-ENG",
3593
+ admin: null,
3594
+ prints: ["buckinghamshire"],
3595
+ codes: ["GB-BKM", "GBR-BKM"],
3596
+ listedCutsOnly: true
3597
+ },
3598
+ {
3599
+ id: "GBR-BRK",
3600
+ kind: "subdivision",
3601
+ name: "Berkshire",
3602
+ geo: "GBR-ENG",
3603
+ admin: null,
3604
+ prints: ["berkshire"],
3605
+ codes: [],
3606
+ listedCutsOnly: true
3607
+ },
3608
+ {
3609
+ id: "GBR-CAM",
3610
+ kind: "subdivision",
3611
+ name: "Cambridgeshire",
3612
+ geo: "GBR-ENG",
3613
+ admin: null,
3614
+ prints: ["cambridgeshire"],
3615
+ codes: ["GB-CAM", "GBR-CAM"],
3616
+ listedCutsOnly: true
3617
+ },
3618
+ {
3619
+ id: "GBR-DUR",
3620
+ kind: "subdivision",
3621
+ name: "County Durham",
3622
+ geo: "GBR-ENG",
3623
+ admin: null,
3624
+ prints: ["co durham", "county durham", "durham"],
3625
+ codes: ["GB-DUR", "GBR-DUR"],
3626
+ listedCutsOnly: true
3627
+ },
3628
+ {
3629
+ id: "GBR-ERY",
3630
+ kind: "subdivision",
3631
+ name: "East Riding of Yorkshire",
3632
+ geo: "GBR-ENG",
3633
+ admin: null,
3634
+ prints: ["east riding", "east riding of yorkshire", "east riding yorkshire"],
3635
+ codes: ["GB-ERY", "GBR-ERY"],
3636
+ listedCutsOnly: true
3637
+ },
3638
+ {
3639
+ id: "GBR-ESX",
3640
+ kind: "subdivision",
3641
+ name: "East Sussex",
3642
+ geo: "GBR-ENG",
3643
+ admin: null,
3644
+ prints: ["east sussex"],
3645
+ codes: ["GB-ESX", "GBR-ESX"],
3646
+ listedCutsOnly: true
3647
+ },
3648
+ {
3649
+ id: "GBR-GTL",
3650
+ kind: "subdivision",
3651
+ name: "Greater London",
3652
+ geo: "GBR-ENG",
3653
+ admin: null,
3654
+ prints: ["greater london"],
3655
+ codes: [],
3656
+ listedCutsOnly: true
3657
+ },
3658
+ {
3659
+ id: "GBR-GTM",
3660
+ kind: "subdivision",
3661
+ name: "Greater Manchester",
3662
+ geo: "GBR-ENG",
3663
+ admin: null,
3664
+ prints: ["greater manchester"],
3665
+ codes: [],
3666
+ listedCutsOnly: true
3667
+ },
3668
+ {
3669
+ id: "GBR-HAM",
3670
+ kind: "subdivision",
3671
+ name: "Hampshire",
3672
+ geo: "GBR-ENG",
3673
+ admin: null,
3674
+ prints: ["hampshire"],
3675
+ codes: ["GB-HAM", "GBR-HAM"],
3676
+ listedCutsOnly: true
3677
+ },
3678
+ {
3679
+ id: "GBR-IOW",
3680
+ kind: "subdivision",
3681
+ name: "Isle of Wight",
3682
+ geo: "GBR-ENG",
3683
+ admin: null,
3684
+ prints: ["isle of wight"],
3685
+ codes: ["GB-IOW", "GBR-IOW"],
3686
+ listedCutsOnly: true
3687
+ },
3688
+ {
3689
+ id: "GBR-KEN",
3690
+ kind: "subdivision",
3691
+ name: "Kent",
3692
+ geo: "GBR-ENG",
3693
+ admin: null,
3694
+ prints: ["kent"],
3695
+ codes: ["GB-KEN", "GBR-KEN"],
3696
+ listedCutsOnly: true
3697
+ },
3698
+ {
3699
+ id: "GBR-LIN",
3700
+ kind: "subdivision",
3701
+ name: "Lincolnshire",
3702
+ geo: "GBR-ENG",
3703
+ admin: null,
3704
+ prints: ["lincolnshire"],
3705
+ codes: ["GB-LIN", "GBR-LIN"],
3706
+ listedCutsOnly: true
3707
+ },
3708
+ {
3709
+ id: "GBR-NBL",
3710
+ kind: "subdivision",
3711
+ name: "Northumberland",
3712
+ geo: "GBR-ENG",
3713
+ admin: null,
3714
+ prints: ["northumberland"],
3715
+ codes: ["GB-NBL", "GBR-NBL"],
3716
+ listedCutsOnly: true
3717
+ },
3718
+ {
3719
+ id: "GBR-NTH",
3720
+ kind: "subdivision",
3721
+ name: "Northamptonshire",
3722
+ geo: "GBR-ENG",
3723
+ admin: null,
3724
+ prints: ["northamptonshire", "northants"],
3725
+ codes: ["GB-NTH", "GBR-NTH"],
3726
+ listedCutsOnly: true
3727
+ },
3728
+ {
3729
+ id: "GBR-NTT",
3730
+ kind: "subdivision",
3731
+ name: "Nottinghamshire",
3732
+ geo: "GBR-ENG",
3733
+ admin: null,
3734
+ prints: ["nottinghamshire"],
3735
+ codes: ["GB-NTT", "GBR-NTT"],
3736
+ listedCutsOnly: true
3737
+ },
3738
+ {
3739
+ id: "GBR-NYK",
3740
+ kind: "subdivision",
3741
+ name: "North Yorkshire",
3742
+ geo: "GBR-ENG",
3743
+ admin: null,
3744
+ prints: ["n yorkshire", "north yorkshire"],
3745
+ codes: ["GB-NYK", "GBR-NYK"],
3746
+ listedCutsOnly: true
3747
+ },
3748
+ {
3749
+ id: "GBR-OXF",
3750
+ kind: "subdivision",
3751
+ name: "Oxfordshire",
3752
+ geo: "GBR-ENG",
3753
+ admin: null,
3754
+ prints: ["oxfordshire"],
3755
+ codes: ["GB-OXF", "GBR-OXF"],
3756
+ listedCutsOnly: true
3757
+ },
3758
+ {
3759
+ id: "GBR-RUT",
3760
+ kind: "subdivision",
3761
+ name: "Rutland",
3762
+ geo: "GBR-ENG",
3763
+ admin: null,
3764
+ prints: ["rutland"],
3765
+ codes: ["GB-RUT", "GBR-RUT"],
3766
+ listedCutsOnly: true
3767
+ },
3768
+ {
3769
+ id: "GBR-SOM",
3770
+ kind: "subdivision",
3771
+ name: "Somerset",
3772
+ geo: "GBR-ENG",
3773
+ admin: null,
3774
+ prints: ["somerset"],
3775
+ codes: ["GB-SOM", "GBR-SOM"],
3776
+ listedCutsOnly: true
3777
+ },
3778
+ {
3779
+ id: "GBR-SYK",
3780
+ kind: "subdivision",
3781
+ name: "South Yorkshire",
3782
+ geo: "GBR-ENG",
3783
+ admin: null,
3784
+ prints: ["s yorkshire", "south yorks", "south yorkshire"],
3785
+ codes: [],
3786
+ listedCutsOnly: true
3787
+ },
3788
+ {
3789
+ id: "GBR-TWR",
3790
+ kind: "subdivision",
3791
+ name: "Tyne and Wear",
3792
+ geo: "GBR-ENG",
3793
+ admin: null,
3794
+ prints: ["tyne and wear", "tyne wear"],
3795
+ codes: [],
3796
+ listedCutsOnly: true
3797
+ },
3798
+ {
3799
+ id: "GBR-WIL",
3800
+ kind: "subdivision",
3801
+ name: "Wiltshire",
3802
+ geo: "GBR-ENG",
3803
+ admin: null,
3804
+ prints: ["wiltshire"],
3805
+ codes: ["GB-WIL", "GBR-WIL"],
3806
+ listedCutsOnly: true
3807
+ },
3808
+ {
3809
+ id: "GBR-WMD",
3810
+ kind: "subdivision",
3811
+ name: "West Midlands",
3812
+ geo: "GBR-ENG",
3813
+ admin: null,
3814
+ prints: ["w midlands", "west midlands"],
3815
+ codes: [],
3816
+ listedCutsOnly: true
3817
+ },
3818
+ {
3819
+ id: "GBR-WOR",
3820
+ kind: "subdivision",
3821
+ name: "Worcestershire",
3822
+ geo: "GBR-ENG",
3823
+ admin: null,
3824
+ prints: ["worcestershire"],
3825
+ codes: ["GB-WOR", "GBR-WOR"],
3826
+ listedCutsOnly: true
3827
+ },
3828
+ {
3829
+ id: "GBR-WSX",
3830
+ kind: "subdivision",
3831
+ name: "West Sussex",
3832
+ geo: "GBR-ENG",
3833
+ admin: null,
3834
+ prints: ["west sussex"],
3835
+ codes: ["GB-WSX", "GBR-WSX"],
3836
+ listedCutsOnly: true
3837
+ },
3838
+ {
3839
+ id: "GBR-WYK",
3840
+ kind: "subdivision",
3841
+ name: "West Yorkshire",
3842
+ geo: "GBR-ENG",
3843
+ admin: null,
3844
+ prints: ["w yorkshire", "west yorkshire"],
3845
+ codes: [],
3846
+ listedCutsOnly: true
3847
+ },
3578
3848
  {
3579
3849
  id: "GBR-NIR",
3580
3850
  kind: "subdivision",
@@ -6654,29 +6924,49 @@ var PLACE_NODES = [
6654
6924
  ];
6655
6925
 
6656
6926
  var QUALIFIER_DELIMITER = /[,;:()[\]/–—-]/;
6657
- var NEVER_PLACE = new Map([
6658
- ["de", "name particle: `De <Surname>`, and Spanish `de <Place>`"],
6659
- ["ni", "Irish patronymic particle: `Ní <Surname>`"],
6660
- ["mo", "Irish possessive: `Mo <Word>`"],
6661
- ["na", "Irish article: `Rince na <Word>`"],
6662
- ["in", "Irish preposition: `<Word> In <Word>`; and `Inc` truncated to `In`"],
6663
- ["co", "Company: `<Surname> & Co.`. Bracketed `(US-CO)` is still Colorado"],
6664
- ["id", "Irish Dance: `<Surname> School of ID`"],
6665
- ["sd", "School of Dance: `<Surname> SD`"]
6666
- ]);
6667
- var NEVER_NAME = new Map([
6668
- ["de", "name particle: `De <Surname>`; and Spanish `de <Place>`"],
6669
- ["ni", "Irish patronymic particle: `Ní <Surname>`"],
6670
- ["ui", "Irish patronymic particle: `Uí <Surname>`"],
6671
- ["mo", "Irish possessive: `Mo <Word>`"],
6672
- ["na", "Irish article: `Rince na <Word>`"],
6673
- ["an", "Irish article: `<Word> an Rince`"],
6674
- ["in", "Irish preposition: `<Word> In <Word>`; and `Inc` truncated to `In`"],
6675
- ["co", "Company: `<Surname> & Co`"],
6676
- ["id", "Irish Dance: `ID de <Place>` keys the place, as `Irish Dancing de <Place>` does"],
6677
- ["sd", "School of Dance: `<Surname> SD`"],
6678
- ["da", "`Dance` truncated: `<Name> Academy of Irish Da` keys the name"]
6679
- ]);
6927
+ var SHORT_TOKEN_BARS = [
6928
+ {
6929
+ spelling: "de",
6930
+ place: true,
6931
+ name: true,
6932
+ why: "name particle: `De <Surname>`, and Spanish `de <Place>`"
6933
+ },
6934
+ { spelling: "ni", place: true, name: true, why: "Irish patronymic particle: `Ní <Surname>`" },
6935
+ { spelling: "ui", place: false, name: true, why: "Irish patronymic particle: `Uí <Surname>`" },
6936
+ { spelling: "mo", place: true, name: true, why: "Irish possessive: `Mo <Word>`" },
6937
+ { spelling: "na", place: true, name: true, why: "Irish article: `Rince na <Word>`" },
6938
+ { spelling: "an", place: false, name: true, why: "Irish article: `<Word> an Rince`" },
6939
+ {
6940
+ spelling: "in",
6941
+ place: true,
6942
+ name: true,
6943
+ why: "Irish preposition: `<Word> In <Word>`, and `Inc` truncated to `In`"
6944
+ },
6945
+ {
6946
+ spelling: "co",
6947
+ place: true,
6948
+ name: true,
6949
+ why: "Company: `<Surname> & Co.`. Bracketed `(US-CO)` is still Colorado"
6950
+ },
6951
+ {
6952
+ spelling: "id",
6953
+ place: true,
6954
+ name: true,
6955
+ why: "Irish Dance: `<Surname> School of ID`, and `ID de <Place>` keys the place, as `Irish Dancing de <Place>` does"
6956
+ },
6957
+ { spelling: "sd", place: true, name: true, why: "School of Dance: `<Surname> SD`" },
6958
+ {
6959
+ spelling: "da",
6960
+ place: false,
6961
+ name: true,
6962
+ why: "`Dance` truncated: `<Name> Academy of Irish Da` keys the name"
6963
+ }
6964
+ ];
6965
+ function barred(flag) {
6966
+ return new Map(SHORT_TOKEN_BARS.filter((row) => row[flag]).map((row) => [row.spelling, row.why]));
6967
+ }
6968
+ var NEVER_PLACE = barred("place");
6969
+ var NEVER_NAME = barred("name");
6680
6970
  var GENERIC_TLD = "com|net|org|edu|gov|mil|int|info|biz";
6681
6971
  var WEB_SPAN = new RegExp([
6682
6972
  "[A-Za-z][A-Za-z0-9+.-]*://\\S+",
@@ -7309,6 +7599,10 @@ function createPlaceReader2(options = {}) {
7309
7599
  return false;
7310
7600
  if (a.kind === b.kind)
7311
7601
  return true;
7602
+ const sub = a.kind === "subdivision" ? a : b.kind === "subdivision" ? b : undefined;
7603
+ const town = a.kind === "city" ? a : b.kind === "city" ? b : undefined;
7604
+ if (sub !== undefined && town !== undefined && town.geo !== null && town.geo === sub.geo)
7605
+ return true;
7312
7606
  const ca = countryOf(a);
7313
7607
  const cb = countryOf(b);
7314
7608
  return ca === undefined || cb === undefined || ca.id !== cb.id;
@@ -7343,7 +7637,9 @@ function createPlaceReader2(options = {}) {
7343
7637
  break;
7344
7638
  }
7345
7639
  if (!plural) {
7346
- const spent = members.filter((m) => m.kind === "country" && members.some((o) => o.id !== m.id && isAncestor(m, o)));
7640
+ const tail = reads[reads.length - 1]?.heads ?? [];
7641
+ const closes = (m) => m.kind === "subdivision" && tail.length === 1 && tail[0]?.id === m.id && members.length > 2 && members.every((o) => onTheMap(m, o));
7642
+ const spent = members.filter((m) => m.kind === "country" && members.some((o) => o.id !== m.id && isAncestor(m, o)) || closes(m));
7347
7643
  const inner = members.filter((m) => !spent.includes(m));
7348
7644
  let apart = inner.length > 1 && spent.length > 0;
7349
7645
  for (let a = 0;a < inner.length && apart; a++)
@@ -7515,6 +7811,19 @@ function createPlaceReader2(options = {}) {
7515
7811
  });
7516
7812
  return abroad ? right : undefined;
7517
7813
  }
7814
+ function townInCounty(left, right) {
7815
+ if (left.length === 0 || right.length === 0)
7816
+ return;
7817
+ if (!left.every((l) => l.kind === "city") || !right.every((r) => r.kind === "subdivision"))
7818
+ return;
7819
+ if (left.some((l) => right.some((r) => relates(l, r))))
7820
+ return;
7821
+ const kept = left.filter((l) => {
7822
+ const parent = l.geo === null ? undefined : idx().byId.get(l.geo);
7823
+ return parent !== undefined && right.some((r) => r.id !== parent.id && onTheMap(parent, r));
7824
+ });
7825
+ return kept.length === 0 ? undefined : kept;
7826
+ }
7518
7827
  function read(print, declared = false, smushed = true) {
7519
7828
  const core = unbracket(repair(print));
7520
7829
  if (fold(core) === "")
@@ -7577,13 +7886,14 @@ function createPlaceReader2(options = {}) {
7577
7886
  };
7578
7887
  }
7579
7888
  const qualified = (k) => k === "comma" || k === "space";
7889
+ const listed = (k) => k === "amp" || k === "and" || k === "slash";
7580
7890
  const settled = reads.map((r, n) => {
7581
7891
  const prev = reads[n - 1];
7582
7892
  if (prev === undefined || !qualified(before[n]) || r.truncated)
7583
7893
  return;
7584
7894
  if (n - 1 > 0 && (qualified(before[n - 1]) || !qualified(before[n - 2])))
7585
7895
  return;
7586
- return qualifierAlone(prev.heads, r.heads);
7896
+ return qualifierAlone(prev.heads, r.heads) ?? (prev.truncated || before[n] !== "comma" || listed(before[n + 1]) ? undefined : townInCounty(prev.heads, r.heads));
7587
7897
  });
7588
7898
  settled.forEach((heads, n) => {
7589
7899
  if (heads === undefined)
@@ -7841,8 +8151,41 @@ function createPlaceReader2(options = {}) {
7841
8151
  const base = hit ?? built(print, declared, key);
7842
8152
  if (hit !== undefined)
7843
8153
  hits++;
8154
+ if (declared && typeof ctx === "object" && base.arm === "unread" && base.refusal === null) {
8155
+ const q = unheldQualifier(print, ctx);
8156
+ if (q !== undefined)
8157
+ return q.refine(ctx);
8158
+ }
7844
8159
  return ctx === undefined ? base : base.refine(ctx);
7845
8160
  }
8161
+ function unheldQualifier(print, ctx) {
8162
+ const within = [ctx.country, ctx.near].filter((w) => w !== undefined && w.trim() !== "").flatMap((w) => headsOf(w));
8163
+ if (within.length === 0)
8164
+ return;
8165
+ const core = unbracket(repair(print));
8166
+ const pieces = core.split(SEP);
8167
+ for (let k = 0;k < pieces.length; k += 2)
8168
+ if (pieces[k] === "")
8169
+ return;
8170
+ let at = -1;
8171
+ for (let k = pieces.length - 2;k >= 1 && at === -1; k -= 2)
8172
+ if (kindOf(pieces[k]) === "comma")
8173
+ at = k;
8174
+ if (at === -1)
8175
+ return;
8176
+ const tail = read(pieces.slice(at + 1).join(""), true);
8177
+ if (tail.heads.length > 0 || tail.refusal !== null)
8178
+ return;
8179
+ const town = read(pieces.slice(0, at).join(""), true);
8180
+ if (town.refusal !== null || town.truncated || town.members !== undefined)
8181
+ return;
8182
+ if (town.heads.length === 0 || !town.heads.every((n) => n.kind === "city"))
8183
+ return;
8184
+ const kept = town.heads.filter((h) => within.some((w) => isAncestor(w, h)));
8185
+ if (kept.length === 0)
8186
+ return;
8187
+ return build(print, chainsFor(kept), { ...town, heads: kept }, []);
8188
+ }
7846
8189
  function heading(print, ctx) {
7847
8190
  const held = idx().byHeading.get(fold(repair(print)));
7848
8191
  if (held === undefined)
package/dist/person.js CHANGED
@@ -106,8 +106,8 @@ class RoleRefusedName {
106
106
  return true;
107
107
  }
108
108
  }
109
- function roleName(role, print, order) {
110
- const read = personName(print, order);
109
+ function roleName(role, print, order, factory) {
110
+ const read = factory === undefined ? personName(print, order) : factory.parse(print, order);
111
111
  const sentinel = MATCHERS[role](print);
112
112
  return sentinel === undefined ? read : new RoleRefusedName(read, role, sentinel);
113
113
  }
@@ -116,7 +116,7 @@ function rolePrintsNoName(role, print) {
116
116
  }
117
117
  var dancerPrintsNoName = (print) => rolePrintsNoName("dancer", print);
118
118
  var seatName = (print) => roleName("adjudicator", print, "given-first");
119
- var dancerName = (print, order) => roleName("dancer", print, order);
119
+ var dancerName = (print, order, factory) => roleName("dancer", print, order, factory);
120
120
  var teacherName = (print, order) => roleName("teacher", print, order);
121
121
  export {
122
122
  ADJUDICATOR_SENTINELS,
package/dist/place.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  placesRelation2,
18
18
  probablySamePlace2,
19
19
  sameReading2
20
- } from "./chunks/place-sa6438qr.js";
20
+ } from "./chunks/place-39zj2ymq.js";
21
21
  import"./chunks/place-tcgfvemw.js";
22
22
  export {
23
23
  DEFAULT_PLACE_CAPACITY2 as DEFAULT_PLACE_CAPACITY,
package/dist/promotion.js CHANGED
@@ -122,6 +122,12 @@ function mojibakeRead(print) {
122
122
  return out.replace(/\uFFFD/gu, "?");
123
123
  }
124
124
  function samePromotedName(was, now) {
125
+ if (samePromotedNameRead(was, now))
126
+ return true;
127
+ const soft = was.replace(/\u00c3\u00ad/gu, "?").replace(/\u00ad/gu, "-");
128
+ return soft !== was && samePromotedNameRead(soft, now);
129
+ }
130
+ function samePromotedNameRead(was, now) {
125
131
  const read = questionMarksRead(was, now);
126
132
  if (read === null)
127
133
  return false;
package/dist/school.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  areAllPlaceTokens2,
13
13
  placeRelation2,
14
14
  sameReading2
15
- } from "./chunks/place-sa6438qr.js";
15
+ } from "./chunks/place-39zj2ymq.js";
16
16
  import {
17
17
  validity
18
18
  } from "./chunks/place-q9kwsmzd.js";
@@ -716,7 +716,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
716
716
  kind: "word",
717
717
  position: "suffix",
718
718
  truncatable: true,
719
- glued: true
719
+ glued: true,
720
+ matchKey: true
720
721
  }
721
722
  ],
722
723
  [
@@ -727,7 +728,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
727
728
  kind: "word",
728
729
  position: "suffix",
729
730
  truncatable: false,
730
- glued: false
731
+ glued: false,
732
+ matchKey: false
731
733
  }
732
734
  ],
733
735
  [
@@ -738,7 +740,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
738
740
  kind: "word",
739
741
  position: "suffix",
740
742
  truncatable: false,
741
- glued: false
743
+ glued: false,
744
+ matchKey: false
742
745
  }
743
746
  ],
744
747
  [
@@ -749,7 +752,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
749
752
  kind: "word",
750
753
  position: "suffix",
751
754
  truncatable: false,
752
- glued: false
755
+ glued: false,
756
+ matchKey: false
753
757
  }
754
758
  ],
755
759
  [
@@ -760,7 +764,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
760
764
  kind: "word",
761
765
  position: "suffix",
762
766
  truncatable: true,
763
- glued: true
767
+ glued: true,
768
+ matchKey: true
764
769
  }
765
770
  ],
766
771
  [
@@ -771,7 +776,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
771
776
  kind: "word",
772
777
  position: "suffix",
773
778
  truncatable: false,
774
- glued: false
779
+ glued: false,
780
+ matchKey: false
775
781
  }
776
782
  ],
777
783
  [
@@ -782,7 +788,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
782
788
  kind: "word",
783
789
  position: "suffix",
784
790
  truncatable: false,
785
- glued: false
791
+ glued: false,
792
+ matchKey: false
786
793
  }
787
794
  ],
788
795
  [
@@ -793,7 +800,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
793
800
  kind: "word",
794
801
  position: "prefix",
795
802
  truncatable: false,
796
- glued: false
803
+ glued: false,
804
+ matchKey: false
797
805
  }
798
806
  ],
799
807
  [
@@ -804,7 +812,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
804
812
  kind: "word",
805
813
  position: "prefix",
806
814
  truncatable: false,
807
- glued: false
815
+ glued: false,
816
+ matchKey: false
808
817
  }
809
818
  ],
810
819
  [
@@ -815,7 +824,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
815
824
  kind: "word",
816
825
  position: "prefix",
817
826
  truncatable: false,
818
- glued: false
827
+ glued: false,
828
+ matchKey: true
819
829
  }
820
830
  ],
821
831
  [
@@ -826,7 +836,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
826
836
  kind: "phrase",
827
837
  position: "prefix",
828
838
  truncatable: false,
829
- glued: false
839
+ glued: false,
840
+ matchKey: true
830
841
  }
831
842
  ],
832
843
  [
@@ -837,7 +848,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
837
848
  kind: "phrase",
838
849
  position: "prefix",
839
850
  truncatable: false,
840
- glued: false
851
+ glued: false,
852
+ matchKey: false
841
853
  }
842
854
  ],
843
855
  [
@@ -848,7 +860,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
848
860
  kind: "word",
849
861
  position: "prefix",
850
862
  truncatable: false,
851
- glued: false
863
+ glued: false,
864
+ matchKey: false
852
865
  }
853
866
  ],
854
867
  [
@@ -859,7 +872,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
859
872
  kind: "word",
860
873
  position: "suffix",
861
874
  truncatable: true,
862
- glued: false
875
+ glued: false,
876
+ matchKey: true
863
877
  }
864
878
  ],
865
879
  [
@@ -870,7 +884,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
870
884
  kind: "word",
871
885
  position: "suffix",
872
886
  truncatable: false,
873
- glued: false
887
+ glued: false,
888
+ matchKey: false
874
889
  }
875
890
  ],
876
891
  [
@@ -881,7 +896,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
881
896
  kind: "word",
882
897
  position: "suffix",
883
898
  truncatable: true,
884
- glued: false
899
+ glued: false,
900
+ matchKey: true
885
901
  }
886
902
  ],
887
903
  [
@@ -892,7 +908,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
892
908
  kind: "word",
893
909
  position: "suffix",
894
910
  truncatable: true,
895
- glued: false
911
+ glued: false,
912
+ matchKey: true
896
913
  }
897
914
  ],
898
915
  [
@@ -903,7 +920,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
903
920
  kind: "word",
904
921
  position: "suffix",
905
922
  truncatable: true,
906
- glued: false
923
+ glued: false,
924
+ matchKey: true
907
925
  }
908
926
  ],
909
927
  [
@@ -914,7 +932,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
914
932
  kind: "word",
915
933
  position: "suffix",
916
934
  truncatable: false,
917
- glued: false
935
+ glued: false,
936
+ matchKey: true
918
937
  }
919
938
  ],
920
939
  [
@@ -925,7 +944,8 @@ var SCHOOL_INSTITUTION_VOCABULARY = new Map([
925
944
  kind: "word",
926
945
  position: "suffix",
927
946
  truncatable: false,
928
- glued: false
947
+ glued: false,
948
+ matchKey: false
929
949
  }
930
950
  ]
931
951
  ]);
@@ -958,28 +978,14 @@ function truncatedInstitution(tail) {
958
978
  }
959
979
  return found;
960
980
  }
981
+ var MATCH_KEY_INSTITUTION_WORDS = [
982
+ ...new Set([...SCHOOL_INSTITUTION_VOCABULARY].filter(([, v]) => v.matchKey).flatMap(([k]) => k.split(" ")))
983
+ ].sort();
961
984
 
962
985
  import { normalizeApostrophes } from "@finbheara/names/lexicon";
963
986
  var SCHOOL_FLUFF_WORDS = new Set([
964
- "academy",
965
- "irish",
966
- "dance",
967
- "dancing",
968
- "dancers",
969
- "school",
970
- "company",
971
- "traditional",
972
- "the",
973
- "of",
974
- "and",
975
- "studio",
976
- "center",
977
- "centre",
978
- "institute",
979
- "scoil",
980
- "rince",
981
- "na",
982
- "an"
987
+ ...[...SCHOOL_FLUFF_VOCABULARY.values()].filter((entry) => entry.tokenFluff).map((entry) => entry.canonical),
988
+ ...MATCH_KEY_INSTITUTION_WORDS
983
989
  ]);
984
990
  var SCHOOL_GENERIC_NAME_PARTS = new Set(["celtic", "clann"]);
985
991
  function hasSpecificToken(tokens) {
@@ -1142,8 +1148,9 @@ function createSchoolNameReader(options = {}) {
1142
1148
  function readSpacingAcute(print) {
1143
1149
  return print.replace(SPACING_ACUTE_AFTER_VOWEL, "$1́").replace(SPACING_ACUTE, "'");
1144
1150
  }
1151
+ const INJECTED_LINK = /(?<=^|[\s([])(?:https?:\/\/|www\.)[^\s)\]]+/giu;
1145
1152
  function renderSn1(print) {
1146
- return normalizeApostrophes2(readSpacingAcute(repairMojibake(print)).normalize("NFKC")).replace(/\p{Cf}+/gu, "").replace(/\p{Cc}+/gu, " ").replace(/\s+/gu, " ").trim();
1153
+ return normalizeApostrophes2(readSpacingAcute(repairMojibake(print)).normalize("NFKC")).replace(/\p{Cf}+/gu, "").replace(/\p{Cc}+/gu, " ").replace(INJECTED_LINK, " ").replace(/\s+/gu, " ").trim();
1147
1154
  }
1148
1155
  const COMPOUND_OPENERS = new Set([
1149
1156
  "hyphen",
@@ -25,7 +25,7 @@ export type AgeArm = "band" | "yob" | "open" | "empty";
25
25
  export type AgeKind = "under" | "over" | "range" | "adult" | "senior" | "yob";
26
26
  export type AgeRefusal = "not-a-band" | "backwards-range" | "overlapping-ranges" | "senior-without-ladder" | "below-youngest-band" | "year-out-of-range";
27
27
  export type AgeSource = "sibling-narrowed" | "printed-band" | "printed-range" | "printed-yob" | "open";
28
- export type AgeClause = "under-floored-on-rung" | "youngest-absorbs" | "over-ceiling-from-next-over" | "over-ceiling-refused-gap" | "and-over-no-ceiling" | "adult-under-ceiling" | "adult-band" | "range-overlapping" | "range-as-under" | "range-top-meets-over" | "range-adult-floor";
28
+ export type AgeClause = "under-floored-on-rung" | "under-stride" | "youngest-absorbs" | "over-ceiling-from-next-over" | "over-ceiling-refused-gap" | "and-over-no-ceiling" | "adult-under-ceiling" | "adult-band" | "range-overlapping" | "range-as-under" | "range-top-meets-over" | "range-adult-floor";
29
29
  export type AgeRelation = "same" | "contains" | "overlaps" | "disjoint" | "silent";
30
30
  export type AgeRelationReason = "equal" | "a-contains-b" | "b-contains-a" | "partial" | "a-older" | "a-younger" | "a-not-a-band" | "b-not-a-band";
31
31
  export interface AgeComparison {
@@ -1,4 +1,4 @@
1
- import { type ParseOrder, type PersonName } from "@finbheara/names/normalize";
1
+ import { type ParseOrder, type PersonName, type PersonNameFactory } from "@finbheara/names/normalize";
2
2
  import { type RowSentinel } from "./row-sentinels.ts";
3
3
  export { SHARED_ROW_SENTINELS, sentinelIn } from "./row-sentinels.ts";
4
4
  export type { RowSentinel } from "./row-sentinels.ts";
@@ -8,9 +8,9 @@ export declare const DANCER_SENTINELS: readonly RowSentinel[];
8
8
  export declare const TEACHER_SENTINELS: readonly RowSentinel[];
9
9
  export declare const ROLE_SENTINELS: Readonly<Record<PersonRole, readonly RowSentinel[]>>;
10
10
  export declare function roleSentinelOf(role: PersonRole, print: string): RowSentinel | undefined;
11
- export declare function roleName(role: PersonRole, print: string, order?: ParseOrder): PersonName;
11
+ export declare function roleName(role: PersonRole, print: string, order?: ParseOrder, factory?: Pick<PersonNameFactory, "parse">): PersonName;
12
12
  export declare function rolePrintsNoName(role: PersonRole, print: string): boolean;
13
13
  export declare const dancerPrintsNoName: (print: string) => boolean;
14
14
  export declare const seatName: (print: string) => PersonName;
15
- export declare const dancerName: (print: string, order?: ParseOrder) => PersonName;
15
+ export declare const dancerName: (print: string, order?: ParseOrder, factory?: Pick<PersonNameFactory, "parse">) => PersonName;
16
16
  export declare const teacherName: (print: string, order?: ParseOrder) => PersonName;
@@ -8,6 +8,7 @@ export interface SchoolInstitution {
8
8
  position: InstitutionPosition;
9
9
  truncatable: boolean;
10
10
  glued: boolean;
11
+ matchKey: boolean;
11
12
  }
12
13
  export declare const SCHOOL_INSTITUTION_VOCABULARY: ReadonlyMap<string, SchoolInstitution>;
13
14
  export declare function schoolInstitution(text: string): SchoolInstitution | undefined;
@@ -18,3 +19,4 @@ export declare const INSTITUTION_TYPES: readonly InstitutionType[];
18
19
  export declare const INSTITUTION_PHRASES: readonly string[];
19
20
  export declare const GLUED_INSTITUTION_SUFFIXES: readonly string[];
20
21
  export declare function truncatedInstitution(tail: string): SchoolInstitution | undefined;
22
+ export declare const MATCH_KEY_INSTITUTION_WORDS: readonly string[];
@@ -1,5 +1,4 @@
1
1
  import type { InstitutionType as RegisterType } from "./school-institution-vocabulary.ts";
2
- export declare const LEGACY_EXCEPTION_PHRASES: ReadonlySet<string>;
3
2
  export type SchoolTokenClass = "NAME_PART" | "INSTITUTION_TYPE" | "FLUFF" | "LOCATION_HINT";
4
3
  export interface SchoolToken {
5
4
  text: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.13.1",
3
+ "version": "0.19.0",
4
4
  "description": "Places, CLRG regions, school names, ages, dances and a person's print in its role, as they are printed: read, compared and told apart",
5
5
  "keywords": [
6
6
  "places",