@finbheara/domain 0.13.0 → 0.18.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
@@ -271,7 +271,7 @@ Age.fromTitle("Beginner Adults Reel U40").token; // "adult u40": the adul
271
271
  Age.fromTitle("Solo Championship - Adult O40").token; // "adult o40": adult, and 40 or over
272
272
  Age.fromTitle("Adult Championship 18 - 29").token; // "adult 18-29": adult, and the range
273
273
  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
274
+ Age.fromTitle("Senior Adult Ladies 19 & Over").token; // "o19": TIDA's oldest youth championship
275
275
  Age.fromTitle("Senior Adult Men 17 & Over").token; // "o17": it admits under-18s
276
276
  Age.fromTitle("Championship (Adult) (A40U)").token; // "adult u40": a split adult code
277
277
  Age.fromTitle("Traditional Set (A40O)").token; // "adult o40"
@@ -288,10 +288,10 @@ An adult word beside an and-over from 18 or a range from 18 reads both as well:
288
288
  `adult 18-29` are adult (`isAdult()`), and their `kind`, interval and birth years are the ones the
289
289
  band alone reads on the same ladder, a range floored at 18 where no rung sits below it. So
290
290
  `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`.
291
+ 18 or a range opening under 18 (`Teen & Adult 13 & Over`) admits youth, and the band stands. TIDA's
292
+ `Senior Adult` beside an and-over is its oldest youth championship, an open championship that
293
+ qualifies for the Worlds, so `Senior Adult Ladies 19 & Over` is `o19`. A closing split
294
+ adult code keeps its split: `(A40U)` is `adult u40`, `(A40O)` and `(A40&O)` are `adult o40`.
295
295
 
296
296
  `adultBeside(band)` is that reading as one function, the band beside the adult word as one token,
297
297
  or `undefined` where the band admits youth. A caller holding a band from another field asks it: a
package/dist/age.js CHANGED
@@ -4,14 +4,16 @@ 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
12
  var AND_UNDER = /(?<![a-z0-9])(\d{1,2})\s*(?:years?\s*)?(?:&|\/|\band\b)\s*u(?:nder)?(?![a-z])(?!\s*\d)/i;
13
13
  var AND_UNDER_RANGE = /(?<![a-z0-9])(\d{1,2})\s*(?:&|\band\b)\s*under\s*(\d{1,2})(?![a-z0-9])/i;
14
14
  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;
15
+ var SLASH_RANGE = /(?<![a-z0-9/])(\d{1,2})\s*\/\s*(\d{1,2})(?=\s*(?:years?|yrs?|boys?|girls?|ladies|men)\b)/i;
16
+ var AGE_N = /(?<!\b(?:under|over)\s*)\bage\s*(\d{1,2})(?![a-z0-9])(?!\s*-?\s*hand)/i;
15
17
  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
18
  var RANGE_DASH = String.raw`(?:\u00c2?[-\u0096–—]|\bto\b)`;
17
19
  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 +30,8 @@ function at(title, offset, start, end, token) {
28
30
  return { token, span: { start: a, end: b, text: title.slice(a, b) } };
29
31
  }
30
32
  function inFence(a, b) {
33
+ if (a.startsWith("0") || b.startsWith("0"))
34
+ return false;
31
35
  const lo = Number(a);
32
36
  const hi = Number(b);
33
37
  return lo >= TITLE_RANGE_MIN && hi <= TITLE_RANGE_MAX && lo < hi;
@@ -44,6 +48,9 @@ function readAnchored(title, s, offset, aside = false) {
44
48
  const j = aside ? null : AND_RANGE.exec(s);
45
49
  if (j?.[1] !== undefined && j[2] !== undefined && inFence(j[1], j[2]))
46
50
  return at(title, offset, j.index, j.index + j[0].length, `${Number(j[1])}-${Number(j[2])}`);
51
+ const sl = aside ? null : SLASH_RANGE.exec(s);
52
+ if (sl?.[1] !== undefined && sl[2] !== undefined && inFence(sl[1], sl[2]) && Number(sl[2]) === Number(sl[1]) + 1)
53
+ return at(title, offset, sl.index, sl.index + sl[0].length, `${Number(sl[1])}-${Number(sl[2])}`);
47
54
  const l = LEAD_RANGE.exec(s);
48
55
  if (l?.[1] !== undefined && l[2] !== undefined && inFence(l[1], l[2])) {
49
56
  const lead = l[0].length - l[0].trimStart().length;
@@ -60,7 +67,7 @@ function readAnchored(title, s, offset, aside = false) {
60
67
  const m = UNDER.exec(s);
61
68
  if (m?.[1] !== undefined)
62
69
  return at(title, offset, m.index, m.index + m[0].length, `u${Number(m[1])}`);
63
- const y = YEARS.exec(s);
70
+ const y = YEARS.exec(s) ?? AGE_N.exec(s);
64
71
  if (y?.[1] !== undefined)
65
72
  return at(title, offset, y.index, y.index + y[0].length, `u${Number(y[1]) + 1}`);
66
73
  const p = OVER_PLUS.exec(s);
@@ -111,6 +118,7 @@ function ageTitleToken(title) {
111
118
  return adultAside !== null && (words.token === "" || words.token === "adult") ? adultAside : adultOverUnder(title, s, lead, words);
112
119
  }
113
120
  var ADULT_UNDER_MIN = 18;
121
+ var SENIOR_ADULT_OVER = /\bsenior\s+adult\b/i;
114
122
  function adultBeside(band) {
115
123
  const b = band.trim().toLowerCase();
116
124
  if (b === "")
@@ -132,6 +140,8 @@ function adultOverUnder(title, s, lead, t) {
132
140
  const a = ADULT.exec(s);
133
141
  if (a === null)
134
142
  return t;
143
+ if (/^o\d+$/.test(t.token) && SENIOR_ADULT_OVER.test(s))
144
+ return t;
135
145
  const token = adultBeside(t.token);
136
146
  if (token === undefined || token === t.token)
137
147
  return t;
@@ -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",
@@ -6943,9 +7213,11 @@ function createPlaceReader2(options = {}) {
6943
7213
  ancestorsOf.set(n.id, out);
6944
7214
  }
6945
7215
  const phrases = [...byPrint.keys()].filter((k) => k.includes(" ")).sort((a, b) => b.length - a.length);
7216
+ const printsSorted = [...byPrint.keys()].map((k, i) => [k, i]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
6946
7217
  return {
6947
7218
  byId,
6948
7219
  byPrint,
7220
+ printsSorted,
6949
7221
  byCode,
6950
7222
  byTruncation,
6951
7223
  byCountryCode,
@@ -7190,13 +7462,32 @@ function createPlaceReader2(options = {}) {
7190
7462
  function completions(folded) {
7191
7463
  if (folded.length < MIN_STEM)
7192
7464
  return [];
7465
+ const i = idx();
7466
+ const sorted = i.printsSorted;
7467
+ let lo = 0;
7468
+ let hi = sorted.length;
7469
+ while (lo < hi) {
7470
+ const mid = lo + hi >>> 1;
7471
+ if (sorted[mid][0] < folded)
7472
+ lo = mid + 1;
7473
+ else
7474
+ hi = mid;
7475
+ }
7476
+ const run = [];
7477
+ for (let k = lo;k < sorted.length; k++) {
7478
+ const at = sorted[k];
7479
+ if (!at[0].startsWith(folded))
7480
+ break;
7481
+ run.push(at);
7482
+ }
7483
+ run.sort((a, b) => a[1] - b[1]);
7193
7484
  const out = [];
7194
- for (const [key, found] of idx().byPrint) {
7195
- if (key.length <= folded.length || !key.startsWith(folded))
7485
+ for (const [key] of run) {
7486
+ if (key.length <= folded.length)
7196
7487
  continue;
7197
7488
  if (key[folded.length] === " ")
7198
7489
  continue;
7199
- for (const n of found)
7490
+ for (const n of i.byPrint.get(key))
7200
7491
  if (n.kind !== "body" && n.listedCutsOnly !== true)
7201
7492
  out.push({ node: n, length: key.length });
7202
7493
  }
@@ -7288,6 +7579,10 @@ function createPlaceReader2(options = {}) {
7288
7579
  return false;
7289
7580
  if (a.kind === b.kind)
7290
7581
  return true;
7582
+ const sub = a.kind === "subdivision" ? a : b.kind === "subdivision" ? b : undefined;
7583
+ const town = a.kind === "city" ? a : b.kind === "city" ? b : undefined;
7584
+ if (sub !== undefined && town !== undefined && town.geo !== null && town.geo === sub.geo)
7585
+ return true;
7291
7586
  const ca = countryOf(a);
7292
7587
  const cb = countryOf(b);
7293
7588
  return ca === undefined || cb === undefined || ca.id !== cb.id;
@@ -7322,7 +7617,9 @@ function createPlaceReader2(options = {}) {
7322
7617
  break;
7323
7618
  }
7324
7619
  if (!plural) {
7325
- const spent = members.filter((m) => m.kind === "country" && members.some((o) => o.id !== m.id && isAncestor(m, o)));
7620
+ const tail = reads[reads.length - 1]?.heads ?? [];
7621
+ const closes = (m) => m.kind === "subdivision" && tail.length === 1 && tail[0]?.id === m.id && members.length > 2 && members.every((o) => onTheMap(m, o));
7622
+ const spent = members.filter((m) => m.kind === "country" && members.some((o) => o.id !== m.id && isAncestor(m, o)) || closes(m));
7326
7623
  const inner = members.filter((m) => !spent.includes(m));
7327
7624
  let apart = inner.length > 1 && spent.length > 0;
7328
7625
  for (let a = 0;a < inner.length && apart; a++)
@@ -7494,6 +7791,19 @@ function createPlaceReader2(options = {}) {
7494
7791
  });
7495
7792
  return abroad ? right : undefined;
7496
7793
  }
7794
+ function townInCounty(left, right) {
7795
+ if (left.length === 0 || right.length === 0)
7796
+ return;
7797
+ if (!left.every((l) => l.kind === "city") || !right.every((r) => r.kind === "subdivision"))
7798
+ return;
7799
+ if (left.some((l) => right.some((r) => relates(l, r))))
7800
+ return;
7801
+ const kept = left.filter((l) => {
7802
+ const parent = l.geo === null ? undefined : idx().byId.get(l.geo);
7803
+ return parent !== undefined && right.some((r) => r.id !== parent.id && onTheMap(parent, r));
7804
+ });
7805
+ return kept.length === 0 ? undefined : kept;
7806
+ }
7497
7807
  function read(print, declared = false, smushed = true) {
7498
7808
  const core = unbracket(repair(print));
7499
7809
  if (fold(core) === "")
@@ -7556,13 +7866,14 @@ function createPlaceReader2(options = {}) {
7556
7866
  };
7557
7867
  }
7558
7868
  const qualified = (k) => k === "comma" || k === "space";
7869
+ const listed = (k) => k === "amp" || k === "and" || k === "slash";
7559
7870
  const settled = reads.map((r, n) => {
7560
7871
  const prev = reads[n - 1];
7561
7872
  if (prev === undefined || !qualified(before[n]) || r.truncated)
7562
7873
  return;
7563
7874
  if (n - 1 > 0 && (qualified(before[n - 1]) || !qualified(before[n - 2])))
7564
7875
  return;
7565
- return qualifierAlone(prev.heads, r.heads);
7876
+ return qualifierAlone(prev.heads, r.heads) ?? (prev.truncated || before[n] !== "comma" || listed(before[n + 1]) ? undefined : townInCounty(prev.heads, r.heads));
7566
7877
  });
7567
7878
  settled.forEach((heads, n) => {
7568
7879
  if (heads === undefined)
@@ -7820,8 +8131,41 @@ function createPlaceReader2(options = {}) {
7820
8131
  const base = hit ?? built(print, declared, key);
7821
8132
  if (hit !== undefined)
7822
8133
  hits++;
8134
+ if (declared && typeof ctx === "object" && base.arm === "unread" && base.refusal === null) {
8135
+ const q = unheldQualifier(print, ctx);
8136
+ if (q !== undefined)
8137
+ return q.refine(ctx);
8138
+ }
7823
8139
  return ctx === undefined ? base : base.refine(ctx);
7824
8140
  }
8141
+ function unheldQualifier(print, ctx) {
8142
+ const within = [ctx.country, ctx.near].filter((w) => w !== undefined && w.trim() !== "").flatMap((w) => headsOf(w));
8143
+ if (within.length === 0)
8144
+ return;
8145
+ const core = unbracket(repair(print));
8146
+ const pieces = core.split(SEP);
8147
+ for (let k = 0;k < pieces.length; k += 2)
8148
+ if (pieces[k] === "")
8149
+ return;
8150
+ let at = -1;
8151
+ for (let k = pieces.length - 2;k >= 1 && at === -1; k -= 2)
8152
+ if (kindOf(pieces[k]) === "comma")
8153
+ at = k;
8154
+ if (at === -1)
8155
+ return;
8156
+ const tail = read(pieces.slice(at + 1).join(""), true);
8157
+ if (tail.heads.length > 0 || tail.refusal !== null)
8158
+ return;
8159
+ const town = read(pieces.slice(0, at).join(""), true);
8160
+ if (town.refusal !== null || town.truncated || town.members !== undefined)
8161
+ return;
8162
+ if (town.heads.length === 0 || !town.heads.every((n) => n.kind === "city"))
8163
+ return;
8164
+ const kept = town.heads.filter((h) => within.some((w) => isAncestor(w, h)));
8165
+ if (kept.length === 0)
8166
+ return;
8167
+ return build(print, chainsFor(kept), { ...town, heads: kept }, []);
8168
+ }
7825
8169
  function heading(print, ctx) {
7826
8170
  const held = idx().byHeading.get(fold(repair(print)));
7827
8171
  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-17e26rga.js";
20
+ } from "./chunks/place-2hdp50r3.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
@@ -46,7 +46,7 @@ function samePromotedRow(was, now) {
46
46
  }
47
47
  var STOOD_FOR = /^(?:'|(?![A-Za-z])\p{L})$/u;
48
48
  function questionMarksRead(was, now) {
49
- const a = [...collapse(mojibakeRead(was))];
49
+ const a = [...collapse(cp437Read(mojibakeRead(was), now))];
50
50
  if (!a.includes("?"))
51
51
  return a.join("");
52
52
  const b = [...collapse(now)];
@@ -67,6 +67,47 @@ function questionMarksRead(was, now) {
67
67
  }
68
68
  return out.join("");
69
69
  }
70
+ var CP437_LETTERS = new Map([
71
+ [128, "Ç"],
72
+ [129, "ü"],
73
+ [130, "é"],
74
+ [131, "â"],
75
+ [132, "ä"],
76
+ [133, "à"],
77
+ [134, "å"],
78
+ [135, "ç"],
79
+ [136, "ê"],
80
+ [137, "ë"],
81
+ [138, "è"],
82
+ [139, "ï"],
83
+ [140, "î"],
84
+ [141, "ì"],
85
+ [142, "Ä"],
86
+ [143, "Å"],
87
+ [144, "É"],
88
+ [145, "æ"],
89
+ [147, "ô"],
90
+ [148, "ö"],
91
+ [149, "ò"],
92
+ [150, "û"],
93
+ [151, "ù"],
94
+ [152, "ÿ"],
95
+ [153, "Ö"],
96
+ [154, "Ü"],
97
+ [160, "á"],
98
+ [161, "í"],
99
+ [162, "ó"],
100
+ [163, "ú"],
101
+ [164, "ñ"],
102
+ [165, "Ñ"]
103
+ ].map(([byte, letter]) => [String.fromCodePoint(byte), letter]));
104
+ function cp437Read(was, now) {
105
+ const a = [...was];
106
+ const b = [...now];
107
+ if (a.length !== b.length)
108
+ return was;
109
+ return a.map((x, i) => CP437_LETTERS.get(x) === b[i] ? b[i] : x).join("");
110
+ }
70
111
  function mojibakeRead(print) {
71
112
  let out = print;
72
113
  const codes = [...out].map((c) => c.codePointAt(0) ?? 0);
@@ -81,6 +122,12 @@ function mojibakeRead(print) {
81
122
  return out.replace(/\uFFFD/gu, "?");
82
123
  }
83
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) {
84
131
  const read = questionMarksRead(was, now);
85
132
  if (read === null)
86
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-17e26rga.js";
15
+ } from "./chunks/place-2hdp50r3.js";
16
16
  import {
17
17
  validity
18
18
  } from "./chunks/place-q9kwsmzd.js";
@@ -1142,8 +1142,9 @@ function createSchoolNameReader(options = {}) {
1142
1142
  function readSpacingAcute(print) {
1143
1143
  return print.replace(SPACING_ACUTE_AFTER_VOWEL, "$1́").replace(SPACING_ACUTE, "'");
1144
1144
  }
1145
+ const INJECTED_LINK = /(?<=^|[\s([])(?:https?:\/\/|www\.)[^\s)\]]+/giu;
1145
1146
  function renderSn1(print) {
1146
- return normalizeApostrophes2(readSpacingAcute(repairMojibake(print)).normalize("NFKC")).replace(/\p{Cf}+/gu, "").replace(/\p{Cc}+/gu, " ").replace(/\s+/gu, " ").trim();
1147
+ return normalizeApostrophes2(readSpacingAcute(repairMojibake(print)).normalize("NFKC")).replace(/\p{Cf}+/gu, "").replace(/\p{Cc}+/gu, " ").replace(INJECTED_LINK, " ").replace(/\s+/gu, " ").trim();
1147
1148
  }
1148
1149
  const COMPOUND_OPENERS = new Set([
1149
1150
  "hyphen",
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.13.0",
3
+ "version": "0.18.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",