@finbheara/domain 0.13.1 → 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",
@@ -7309,6 +7579,10 @@ function createPlaceReader2(options = {}) {
7309
7579
  return false;
7310
7580
  if (a.kind === b.kind)
7311
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;
7312
7586
  const ca = countryOf(a);
7313
7587
  const cb = countryOf(b);
7314
7588
  return ca === undefined || cb === undefined || ca.id !== cb.id;
@@ -7343,7 +7617,9 @@ function createPlaceReader2(options = {}) {
7343
7617
  break;
7344
7618
  }
7345
7619
  if (!plural) {
7346
- 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));
7347
7623
  const inner = members.filter((m) => !spent.includes(m));
7348
7624
  let apart = inner.length > 1 && spent.length > 0;
7349
7625
  for (let a = 0;a < inner.length && apart; a++)
@@ -7515,6 +7791,19 @@ function createPlaceReader2(options = {}) {
7515
7791
  });
7516
7792
  return abroad ? right : undefined;
7517
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
+ }
7518
7807
  function read(print, declared = false, smushed = true) {
7519
7808
  const core = unbracket(repair(print));
7520
7809
  if (fold(core) === "")
@@ -7577,13 +7866,14 @@ function createPlaceReader2(options = {}) {
7577
7866
  };
7578
7867
  }
7579
7868
  const qualified = (k) => k === "comma" || k === "space";
7869
+ const listed = (k) => k === "amp" || k === "and" || k === "slash";
7580
7870
  const settled = reads.map((r, n) => {
7581
7871
  const prev = reads[n - 1];
7582
7872
  if (prev === undefined || !qualified(before[n]) || r.truncated)
7583
7873
  return;
7584
7874
  if (n - 1 > 0 && (qualified(before[n - 1]) || !qualified(before[n - 2])))
7585
7875
  return;
7586
- 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));
7587
7877
  });
7588
7878
  settled.forEach((heads, n) => {
7589
7879
  if (heads === undefined)
@@ -7841,8 +8131,41 @@ function createPlaceReader2(options = {}) {
7841
8131
  const base = hit ?? built(print, declared, key);
7842
8132
  if (hit !== undefined)
7843
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
+ }
7844
8139
  return ctx === undefined ? base : base.refine(ctx);
7845
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
+ }
7846
8169
  function heading(print, ctx) {
7847
8170
  const held = idx().byHeading.get(fold(repair(print)));
7848
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-sa6438qr.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
@@ -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-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.1",
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",