@finbheara/domain 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +273 -2
- package/dist/age.js +420 -0
- package/dist/ceili.js +15 -0
- package/dist/chunks/place-7gqecaxd.js +201 -0
- package/dist/chunks/place-g6yw4cq6.js +134 -0
- package/dist/chunks/place-kr42294h.js +267 -0
- package/dist/chunks/place-qbp3xfb4.js +7412 -0
- package/dist/clrg-regions.js +603 -0
- package/dist/dance-types.js +14 -0
- package/dist/dance.js +41 -0
- package/dist/noun-index.js +233 -0
- package/dist/place.js +36 -0
- package/dist/school.js +2269 -0
- package/dist/tune.js +23 -0
- package/dist/types/age-types.d.ts +31 -0
- package/dist/types/age.d.ts +84 -0
- package/dist/types/ceili.d.ts +53 -0
- package/dist/types/clrg-regions.d.ts +31 -0
- package/dist/types/dance-match.d.ts +28 -0
- package/dist/types/dance-types.d.ts +9 -0
- package/dist/types/dance.d.ts +14 -0
- package/dist/types/noun-index.d.ts +67 -0
- package/dist/types/place-tree.d.ts +4 -0
- package/dist/types/place-types.d.ts +24 -0
- package/dist/types/place.d.ts +98 -0
- package/dist/types/school-compare.d.ts +39 -0
- package/dist/types/school-encoder.d.ts +8 -0
- package/dist/types/school-exception-vocabulary.d.ts +23 -0
- package/dist/types/school-fluff-vocabulary.d.ts +16 -0
- package/dist/types/school-fused-prefix.d.ts +1 -0
- package/dist/types/school-identity.d.ts +9 -0
- package/dist/types/school-institution-vocabulary.d.ts +20 -0
- package/dist/types/school-location.d.ts +16 -0
- package/dist/types/school-match-key.d.ts +5 -0
- package/dist/types/school-name.d.ts +82 -0
- package/dist/types/school-tokens.d.ts +20 -0
- package/dist/types/school.d.ts +11 -0
- package/dist/types/tune.d.ts +63 -0
- package/package.json +73 -5
package/dist/school.js
ADDED
|
@@ -0,0 +1,2269 @@
|
|
|
1
|
+
import {
|
|
2
|
+
place2,
|
|
3
|
+
isPlaceToken2,
|
|
4
|
+
countryCodeOf2,
|
|
5
|
+
areAllPlaceTokens2,
|
|
6
|
+
placeRelation2,
|
|
7
|
+
sameReading2
|
|
8
|
+
} from "./chunks/place-qbp3xfb4.js";
|
|
9
|
+
|
|
10
|
+
import { normalizeApostrophes } from "@finbheara/names/lexicon";
|
|
11
|
+
import { SURNAME_SEPARATOR, lexiconKey, titleCaseNamePart } from "@finbheara/names/normalize";
|
|
12
|
+
import { personName } from "@finbheara/names/normalize";
|
|
13
|
+
|
|
14
|
+
import { placeholderOf } from "@finbheara/names/normalize";
|
|
15
|
+
var SCHOOL_NAME_EXCEPTIONS = [
|
|
16
|
+
{
|
|
17
|
+
phrase: "the academy",
|
|
18
|
+
scope: "label",
|
|
19
|
+
as: "NAME_PART",
|
|
20
|
+
token: "theacademy"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
phrase: "the academy irish dance co",
|
|
24
|
+
scope: "label",
|
|
25
|
+
as: "NAME_PART",
|
|
26
|
+
token: "theacademy"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
phrase: "rhythm of ireland",
|
|
30
|
+
scope: "phrase",
|
|
31
|
+
as: "NAME_PART",
|
|
32
|
+
token: "rhythmofireland"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
phrase: "irish dance center",
|
|
36
|
+
scope: "label",
|
|
37
|
+
as: "NAME_PART",
|
|
38
|
+
token: "irishdancecenter"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
phrase: "the irish dance center",
|
|
42
|
+
scope: "label",
|
|
43
|
+
as: "NAME_PART",
|
|
44
|
+
token: "irishdancecenter"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
phrase: "the irish dance academy",
|
|
48
|
+
scope: "label",
|
|
49
|
+
as: "NAME_PART",
|
|
50
|
+
token: "theirishdanceacademy"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
phrase: "irish dance academy",
|
|
54
|
+
scope: "label",
|
|
55
|
+
as: "NAME_PART",
|
|
56
|
+
token: "theirishdanceacademy"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
phrase: "the irish dance company",
|
|
60
|
+
scope: "label",
|
|
61
|
+
as: "NAME_PART",
|
|
62
|
+
token: "theirishdancecompany"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
phrase: "gra na rince",
|
|
66
|
+
scope: "phrase",
|
|
67
|
+
as: "NAME_PART",
|
|
68
|
+
token: "granarince"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
phrase: "clann na cara",
|
|
72
|
+
scope: "phrase",
|
|
73
|
+
as: "NAME_PART",
|
|
74
|
+
token: "clannnacara"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
phrase: "rince academy",
|
|
78
|
+
scope: "label",
|
|
79
|
+
as: "NAME_PART",
|
|
80
|
+
token: "rinceacademy"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
phrase: "i d a s",
|
|
84
|
+
scope: "label",
|
|
85
|
+
as: "NAME_PART",
|
|
86
|
+
token: "idas"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
phrase: "ml dance academy",
|
|
90
|
+
scope: "phrase",
|
|
91
|
+
as: "NAME_PART",
|
|
92
|
+
token: "mldanceacademy"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
phrase: "cm dance",
|
|
96
|
+
scope: "label",
|
|
97
|
+
as: "NAME_PART",
|
|
98
|
+
token: "cmdance"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
phrase: "traditional irish dance academy",
|
|
102
|
+
scope: "label",
|
|
103
|
+
as: "NAME_PART",
|
|
104
|
+
token: "traditionalirishdanceacademy"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
phrase: "irish dance centre",
|
|
108
|
+
scope: "label",
|
|
109
|
+
as: "NAME_PART",
|
|
110
|
+
token: "irishdancecenter"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
phrase: "tj academy",
|
|
114
|
+
scope: "phrase",
|
|
115
|
+
as: "NAME_PART",
|
|
116
|
+
token: "tjacademy"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
phrase: "m d l academy",
|
|
120
|
+
scope: "label",
|
|
121
|
+
as: "NAME_PART",
|
|
122
|
+
token: "mdlacademy"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
phrase: "wa academy",
|
|
126
|
+
scope: "phrase",
|
|
127
|
+
as: "NAME_PART",
|
|
128
|
+
token: "waacademy"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
phrase: "fd academy",
|
|
132
|
+
scope: "phrase",
|
|
133
|
+
as: "NAME_PART",
|
|
134
|
+
token: "fdacademy"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
phrase: "em academy",
|
|
138
|
+
scope: "label",
|
|
139
|
+
as: "NAME_PART",
|
|
140
|
+
token: "emacademy"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
phrase: "aoibhneas an rince",
|
|
144
|
+
scope: "phrase",
|
|
145
|
+
as: "NAME_PART",
|
|
146
|
+
token: "aoibhneasanrince"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
phrase: "an rince mor",
|
|
150
|
+
scope: "phrase",
|
|
151
|
+
as: "NAME_PART",
|
|
152
|
+
token: "anrincemor"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
phrase: "clan rince",
|
|
156
|
+
scope: "phrase",
|
|
157
|
+
as: "NAME_PART",
|
|
158
|
+
token: "clanrince"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
phrase: "acadamh rince",
|
|
162
|
+
scope: "phrase",
|
|
163
|
+
as: "NAME_PART",
|
|
164
|
+
token: "acadamhrince"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
phrase: "spreagadh na rince",
|
|
168
|
+
scope: "phrase",
|
|
169
|
+
as: "NAME_PART",
|
|
170
|
+
token: "spreagadhnarince"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
phrase: "clann na rince",
|
|
174
|
+
scope: "label",
|
|
175
|
+
as: "NAME_PART",
|
|
176
|
+
token: "clannnarince"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
phrase: "mise eire",
|
|
180
|
+
scope: "phrase",
|
|
181
|
+
as: "NAME_PART",
|
|
182
|
+
token: "miseeire"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
phrase: "reel eire",
|
|
186
|
+
scope: "phrase",
|
|
187
|
+
as: "NAME_PART",
|
|
188
|
+
token: "reeleire"
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
var EXCEPTIONS_BY_LENGTH = [
|
|
192
|
+
...SCHOOL_NAME_EXCEPTIONS
|
|
193
|
+
].sort((a, b) => b.phrase.length - a.phrase.length);
|
|
194
|
+
var NOT_A_SCHOOL = [
|
|
195
|
+
{
|
|
196
|
+
pattern: "north west regional council",
|
|
197
|
+
kind: "exact",
|
|
198
|
+
reason: "organization"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
pattern: "^#\\s*\\d+$",
|
|
202
|
+
kind: "regex",
|
|
203
|
+
reason: "placeholder"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
pattern: "cce other",
|
|
207
|
+
kind: "exact",
|
|
208
|
+
reason: "placeholder"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
pattern: "^x{4,}$",
|
|
212
|
+
kind: "regex",
|
|
213
|
+
reason: "placeholder"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
pattern: "^.+['’]s team$",
|
|
217
|
+
kind: "regex",
|
|
218
|
+
reason: "not-a-name"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
pattern: "^\\d+(st|nd|rd|th) team up$",
|
|
222
|
+
kind: "regex",
|
|
223
|
+
reason: "not-a-name"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
pattern: "^[abc] team$",
|
|
227
|
+
kind: "regex",
|
|
228
|
+
reason: "not-a-name"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
pattern: "^[^\\p{L}]*$",
|
|
232
|
+
kind: "regex",
|
|
233
|
+
reason: "not-a-name"
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
var INVISIBLE = /[ ]/g;
|
|
237
|
+
var COMBINING = /[̀-ͯ]/g;
|
|
238
|
+
function normalizeForRefusal(print) {
|
|
239
|
+
return print.replace(INVISIBLE, " ").normalize("NFD").replace(COMBINING, "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
240
|
+
}
|
|
241
|
+
var PLACEHOLDER_ROWS = new Map;
|
|
242
|
+
function placeholderRow(p) {
|
|
243
|
+
let row = PLACEHOLDER_ROWS.get(p);
|
|
244
|
+
if (row === undefined)
|
|
245
|
+
PLACEHOLDER_ROWS.set(p, row = { pattern: p.print, kind: p.match ?? "exact", reason: "placeholder" });
|
|
246
|
+
return row;
|
|
247
|
+
}
|
|
248
|
+
function notASchoolIn(rows) {
|
|
249
|
+
const regexes = new Map(rows.filter((row) => row.kind === "regex").map((row) => [row, new RegExp(row.pattern, "iu")]));
|
|
250
|
+
return (print) => {
|
|
251
|
+
const text = normalizeForRefusal(print);
|
|
252
|
+
if (text === "")
|
|
253
|
+
return;
|
|
254
|
+
const placeholder = placeholderOf(text);
|
|
255
|
+
if (placeholder !== undefined)
|
|
256
|
+
return placeholderRow(placeholder);
|
|
257
|
+
for (const row of rows) {
|
|
258
|
+
if (row.kind === "exact" && text === row.pattern)
|
|
259
|
+
return row;
|
|
260
|
+
if (row.kind === "prefix" && text.startsWith(row.pattern))
|
|
261
|
+
return row;
|
|
262
|
+
if (row.kind === "regex" && regexes.get(row)?.test(text))
|
|
263
|
+
return row;
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function exceptionsByLength(rows) {
|
|
269
|
+
return [...rows].sort((a, b) => b.phrase.length - a.phrase.length);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
var SCHOOL_FLUFF_VOCABULARY = new Map([
|
|
273
|
+
[
|
|
274
|
+
"irish",
|
|
275
|
+
{
|
|
276
|
+
canonical: "irish",
|
|
277
|
+
kind: "word",
|
|
278
|
+
lowerInside: false,
|
|
279
|
+
descriptor: true,
|
|
280
|
+
truncatable: true,
|
|
281
|
+
tokenFluff: true
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
[
|
|
285
|
+
"dance",
|
|
286
|
+
{
|
|
287
|
+
canonical: "dance",
|
|
288
|
+
kind: "word",
|
|
289
|
+
lowerInside: false,
|
|
290
|
+
descriptor: true,
|
|
291
|
+
truncatable: true,
|
|
292
|
+
tokenFluff: true
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
[
|
|
296
|
+
"dancing",
|
|
297
|
+
{
|
|
298
|
+
canonical: "dancing",
|
|
299
|
+
kind: "word",
|
|
300
|
+
lowerInside: false,
|
|
301
|
+
descriptor: true,
|
|
302
|
+
truncatable: true,
|
|
303
|
+
tokenFluff: true
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
[
|
|
307
|
+
"dancers",
|
|
308
|
+
{
|
|
309
|
+
canonical: "dancers",
|
|
310
|
+
kind: "word",
|
|
311
|
+
lowerInside: false,
|
|
312
|
+
descriptor: true,
|
|
313
|
+
truncatable: true,
|
|
314
|
+
tokenFluff: true
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
[
|
|
318
|
+
"traditional",
|
|
319
|
+
{
|
|
320
|
+
canonical: "traditional",
|
|
321
|
+
kind: "word",
|
|
322
|
+
lowerInside: false,
|
|
323
|
+
descriptor: true,
|
|
324
|
+
truncatable: true,
|
|
325
|
+
tokenFluff: true
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
[
|
|
329
|
+
"the",
|
|
330
|
+
{
|
|
331
|
+
canonical: "the",
|
|
332
|
+
kind: "word",
|
|
333
|
+
lowerInside: true,
|
|
334
|
+
descriptor: true,
|
|
335
|
+
truncatable: false,
|
|
336
|
+
tokenFluff: true
|
|
337
|
+
}
|
|
338
|
+
],
|
|
339
|
+
[
|
|
340
|
+
"of",
|
|
341
|
+
{
|
|
342
|
+
canonical: "of",
|
|
343
|
+
kind: "word",
|
|
344
|
+
lowerInside: true,
|
|
345
|
+
descriptor: true,
|
|
346
|
+
truncatable: false,
|
|
347
|
+
tokenFluff: true
|
|
348
|
+
}
|
|
349
|
+
],
|
|
350
|
+
[
|
|
351
|
+
"and",
|
|
352
|
+
{
|
|
353
|
+
canonical: "and",
|
|
354
|
+
kind: "word",
|
|
355
|
+
lowerInside: true,
|
|
356
|
+
descriptor: true,
|
|
357
|
+
truncatable: false,
|
|
358
|
+
tokenFluff: true
|
|
359
|
+
}
|
|
360
|
+
],
|
|
361
|
+
[
|
|
362
|
+
"na",
|
|
363
|
+
{
|
|
364
|
+
canonical: "na",
|
|
365
|
+
kind: "word",
|
|
366
|
+
lowerInside: true,
|
|
367
|
+
descriptor: false,
|
|
368
|
+
truncatable: false,
|
|
369
|
+
tokenFluff: true
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
[
|
|
373
|
+
"an",
|
|
374
|
+
{
|
|
375
|
+
canonical: "an",
|
|
376
|
+
kind: "word",
|
|
377
|
+
lowerInside: true,
|
|
378
|
+
descriptor: false,
|
|
379
|
+
truncatable: false,
|
|
380
|
+
tokenFluff: true
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
[
|
|
384
|
+
"a",
|
|
385
|
+
{
|
|
386
|
+
canonical: "a",
|
|
387
|
+
kind: "word",
|
|
388
|
+
lowerInside: true,
|
|
389
|
+
descriptor: false,
|
|
390
|
+
truncatable: false,
|
|
391
|
+
tokenFluff: false
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
[
|
|
395
|
+
"do",
|
|
396
|
+
{
|
|
397
|
+
canonical: "do",
|
|
398
|
+
kind: "word",
|
|
399
|
+
lowerInside: true,
|
|
400
|
+
descriptor: false,
|
|
401
|
+
truncatable: false,
|
|
402
|
+
tokenFluff: false
|
|
403
|
+
}
|
|
404
|
+
],
|
|
405
|
+
[
|
|
406
|
+
"irish dance",
|
|
407
|
+
{
|
|
408
|
+
canonical: "irish dance",
|
|
409
|
+
kind: "phrase",
|
|
410
|
+
lowerInside: false,
|
|
411
|
+
descriptor: true,
|
|
412
|
+
truncatable: true,
|
|
413
|
+
tokenFluff: false
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
[
|
|
417
|
+
"irish dancing",
|
|
418
|
+
{
|
|
419
|
+
canonical: "irish dancing",
|
|
420
|
+
kind: "phrase",
|
|
421
|
+
lowerInside: false,
|
|
422
|
+
descriptor: true,
|
|
423
|
+
truncatable: true,
|
|
424
|
+
tokenFluff: false
|
|
425
|
+
}
|
|
426
|
+
],
|
|
427
|
+
[
|
|
428
|
+
"of irish dance",
|
|
429
|
+
{
|
|
430
|
+
canonical: "of irish dance",
|
|
431
|
+
kind: "phrase",
|
|
432
|
+
lowerInside: false,
|
|
433
|
+
descriptor: true,
|
|
434
|
+
truncatable: true,
|
|
435
|
+
tokenFluff: false
|
|
436
|
+
}
|
|
437
|
+
],
|
|
438
|
+
[
|
|
439
|
+
"of irish dancing",
|
|
440
|
+
{
|
|
441
|
+
canonical: "of irish dancing",
|
|
442
|
+
kind: "phrase",
|
|
443
|
+
lowerInside: false,
|
|
444
|
+
descriptor: true,
|
|
445
|
+
truncatable: true,
|
|
446
|
+
tokenFluff: false
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
[
|
|
450
|
+
"school of irish dance",
|
|
451
|
+
{
|
|
452
|
+
canonical: "school of irish dance",
|
|
453
|
+
kind: "phrase",
|
|
454
|
+
lowerInside: false,
|
|
455
|
+
descriptor: true,
|
|
456
|
+
truncatable: true,
|
|
457
|
+
tokenFluff: false
|
|
458
|
+
}
|
|
459
|
+
],
|
|
460
|
+
[
|
|
461
|
+
"school of irish dancing",
|
|
462
|
+
{
|
|
463
|
+
canonical: "school of irish dancing",
|
|
464
|
+
kind: "phrase",
|
|
465
|
+
lowerInside: false,
|
|
466
|
+
descriptor: true,
|
|
467
|
+
truncatable: true,
|
|
468
|
+
tokenFluff: false
|
|
469
|
+
}
|
|
470
|
+
],
|
|
471
|
+
[
|
|
472
|
+
"school of dance",
|
|
473
|
+
{
|
|
474
|
+
canonical: "school of dance",
|
|
475
|
+
kind: "phrase",
|
|
476
|
+
lowerInside: false,
|
|
477
|
+
descriptor: true,
|
|
478
|
+
truncatable: true,
|
|
479
|
+
tokenFluff: false
|
|
480
|
+
}
|
|
481
|
+
],
|
|
482
|
+
[
|
|
483
|
+
"school of dancing",
|
|
484
|
+
{
|
|
485
|
+
canonical: "school of dancing",
|
|
486
|
+
kind: "phrase",
|
|
487
|
+
lowerInside: false,
|
|
488
|
+
descriptor: true,
|
|
489
|
+
truncatable: true,
|
|
490
|
+
tokenFluff: false
|
|
491
|
+
}
|
|
492
|
+
],
|
|
493
|
+
[
|
|
494
|
+
"irish dance school",
|
|
495
|
+
{
|
|
496
|
+
canonical: "irish dance school",
|
|
497
|
+
kind: "phrase",
|
|
498
|
+
lowerInside: false,
|
|
499
|
+
descriptor: true,
|
|
500
|
+
truncatable: true,
|
|
501
|
+
tokenFluff: false
|
|
502
|
+
}
|
|
503
|
+
],
|
|
504
|
+
[
|
|
505
|
+
"irish dancing school",
|
|
506
|
+
{
|
|
507
|
+
canonical: "irish dancing school",
|
|
508
|
+
kind: "phrase",
|
|
509
|
+
lowerInside: false,
|
|
510
|
+
descriptor: true,
|
|
511
|
+
truncatable: true,
|
|
512
|
+
tokenFluff: false
|
|
513
|
+
}
|
|
514
|
+
],
|
|
515
|
+
[
|
|
516
|
+
"irish dance academy",
|
|
517
|
+
{
|
|
518
|
+
canonical: "irish dance academy",
|
|
519
|
+
kind: "phrase",
|
|
520
|
+
lowerInside: false,
|
|
521
|
+
descriptor: true,
|
|
522
|
+
truncatable: true,
|
|
523
|
+
tokenFluff: false
|
|
524
|
+
}
|
|
525
|
+
],
|
|
526
|
+
[
|
|
527
|
+
"dance academy",
|
|
528
|
+
{
|
|
529
|
+
canonical: "dance academy",
|
|
530
|
+
kind: "phrase",
|
|
531
|
+
lowerInside: false,
|
|
532
|
+
descriptor: true,
|
|
533
|
+
truncatable: true,
|
|
534
|
+
tokenFluff: false
|
|
535
|
+
}
|
|
536
|
+
],
|
|
537
|
+
[
|
|
538
|
+
"irish step dance",
|
|
539
|
+
{
|
|
540
|
+
canonical: "irish step dance",
|
|
541
|
+
kind: "phrase",
|
|
542
|
+
lowerInside: false,
|
|
543
|
+
descriptor: true,
|
|
544
|
+
truncatable: true,
|
|
545
|
+
tokenFluff: false
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
[
|
|
549
|
+
"irish step dancing",
|
|
550
|
+
{
|
|
551
|
+
canonical: "irish step dancing",
|
|
552
|
+
kind: "phrase",
|
|
553
|
+
lowerInside: false,
|
|
554
|
+
descriptor: true,
|
|
555
|
+
truncatable: true,
|
|
556
|
+
tokenFluff: false
|
|
557
|
+
}
|
|
558
|
+
]
|
|
559
|
+
]);
|
|
560
|
+
function schoolFluff(text) {
|
|
561
|
+
return SCHOOL_FLUFF_VOCABULARY.get(text.toLocaleLowerCase());
|
|
562
|
+
}
|
|
563
|
+
function isFluffWord(token) {
|
|
564
|
+
return schoolFluff(token)?.kind === "word";
|
|
565
|
+
}
|
|
566
|
+
function isTruncatedFluff(token) {
|
|
567
|
+
const t = token.toLocaleLowerCase();
|
|
568
|
+
if (t === "")
|
|
569
|
+
return false;
|
|
570
|
+
for (const entry of SCHOOL_FLUFF_VOCABULARY.values()) {
|
|
571
|
+
if (entry.kind !== "word" || !entry.truncatable)
|
|
572
|
+
continue;
|
|
573
|
+
if (entry.canonical.length > t.length && entry.canonical.startsWith(t))
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
var FLUFF_PHRASES = [...SCHOOL_FLUFF_VOCABULARY.values()].filter((entry) => entry.kind === "phrase").map((entry) => entry.canonical).sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
579
|
+
function isFluffPhrasePrefix(text) {
|
|
580
|
+
const t = text.toLocaleLowerCase();
|
|
581
|
+
if (t === "")
|
|
582
|
+
return false;
|
|
583
|
+
return FLUFF_PHRASES.some((phrase) => phrase.startsWith(t));
|
|
584
|
+
}
|
|
585
|
+
function trailingFluffRun(tokens, distinctive) {
|
|
586
|
+
let start = tokens.length;
|
|
587
|
+
for (let i = tokens.length - 1;i >= 0; i -= 1) {
|
|
588
|
+
if (!isFluffPhrasePrefix(tokens.slice(i).join(" ")))
|
|
589
|
+
continue;
|
|
590
|
+
if (!tokens.slice(0, i).some(distinctive))
|
|
591
|
+
continue;
|
|
592
|
+
start = i;
|
|
593
|
+
}
|
|
594
|
+
while (start > 0) {
|
|
595
|
+
const token = tokens[start - 1] ?? "";
|
|
596
|
+
if (!isFluffWord(token) && !isTruncatedFluff(token) && distinctive(token))
|
|
597
|
+
break;
|
|
598
|
+
if (!tokens.slice(0, start - 1).some(distinctive))
|
|
599
|
+
break;
|
|
600
|
+
start -= 1;
|
|
601
|
+
}
|
|
602
|
+
return start;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
import { nameParticle } from "@finbheara/names/particles";
|
|
606
|
+
var FUSED_PREFIXES = new Set(["O", "Mc", "Mac"]);
|
|
607
|
+
function joinedPrefix(printed, next) {
|
|
608
|
+
if (!/^\p{L}{2}/u.test(next))
|
|
609
|
+
return;
|
|
610
|
+
const particle = nameParticle(printed);
|
|
611
|
+
if (particle === undefined || !particle.standalone)
|
|
612
|
+
return;
|
|
613
|
+
if (!FUSED_PREFIXES.has(particle.canonical))
|
|
614
|
+
return;
|
|
615
|
+
return particle.join === "apostrophe" ? `${particle.canonical}'${next}` : `${particle.canonical}${next}`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
var SCHOOL_INSTITUTION_VOCABULARY = new Map([
|
|
619
|
+
[
|
|
620
|
+
"academy",
|
|
621
|
+
{
|
|
622
|
+
canonical: "academy",
|
|
623
|
+
label: "Academy",
|
|
624
|
+
kind: "word",
|
|
625
|
+
position: "suffix",
|
|
626
|
+
truncatable: true,
|
|
627
|
+
glued: true
|
|
628
|
+
}
|
|
629
|
+
],
|
|
630
|
+
[
|
|
631
|
+
"acadamy",
|
|
632
|
+
{
|
|
633
|
+
canonical: "academy",
|
|
634
|
+
label: "Academy",
|
|
635
|
+
kind: "word",
|
|
636
|
+
position: "suffix",
|
|
637
|
+
truncatable: false,
|
|
638
|
+
glued: false
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
[
|
|
642
|
+
"acadamh",
|
|
643
|
+
{
|
|
644
|
+
canonical: "academy",
|
|
645
|
+
label: "Academy",
|
|
646
|
+
kind: "word",
|
|
647
|
+
position: "suffix",
|
|
648
|
+
truncatable: false,
|
|
649
|
+
glued: false
|
|
650
|
+
}
|
|
651
|
+
],
|
|
652
|
+
[
|
|
653
|
+
"accademia",
|
|
654
|
+
{
|
|
655
|
+
canonical: "academy",
|
|
656
|
+
label: "Academy",
|
|
657
|
+
kind: "word",
|
|
658
|
+
position: "suffix",
|
|
659
|
+
truncatable: false,
|
|
660
|
+
glued: false
|
|
661
|
+
}
|
|
662
|
+
],
|
|
663
|
+
[
|
|
664
|
+
"school",
|
|
665
|
+
{
|
|
666
|
+
canonical: "school",
|
|
667
|
+
label: "School",
|
|
668
|
+
kind: "word",
|
|
669
|
+
position: "suffix",
|
|
670
|
+
truncatable: true,
|
|
671
|
+
glued: true
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
[
|
|
675
|
+
"schools",
|
|
676
|
+
{
|
|
677
|
+
canonical: "school",
|
|
678
|
+
label: "School",
|
|
679
|
+
kind: "word",
|
|
680
|
+
position: "suffix",
|
|
681
|
+
truncatable: false,
|
|
682
|
+
glued: false
|
|
683
|
+
}
|
|
684
|
+
],
|
|
685
|
+
[
|
|
686
|
+
"sd",
|
|
687
|
+
{
|
|
688
|
+
canonical: "school",
|
|
689
|
+
label: "School",
|
|
690
|
+
kind: "word",
|
|
691
|
+
position: "suffix",
|
|
692
|
+
truncatable: false,
|
|
693
|
+
glued: false
|
|
694
|
+
}
|
|
695
|
+
],
|
|
696
|
+
[
|
|
697
|
+
"ecole",
|
|
698
|
+
{
|
|
699
|
+
canonical: "school",
|
|
700
|
+
label: "School",
|
|
701
|
+
kind: "word",
|
|
702
|
+
position: "prefix",
|
|
703
|
+
truncatable: false,
|
|
704
|
+
glued: false
|
|
705
|
+
}
|
|
706
|
+
],
|
|
707
|
+
[
|
|
708
|
+
"escuela",
|
|
709
|
+
{
|
|
710
|
+
canonical: "school",
|
|
711
|
+
label: "School",
|
|
712
|
+
kind: "word",
|
|
713
|
+
position: "prefix",
|
|
714
|
+
truncatable: false,
|
|
715
|
+
glued: false
|
|
716
|
+
}
|
|
717
|
+
],
|
|
718
|
+
[
|
|
719
|
+
"scoil",
|
|
720
|
+
{
|
|
721
|
+
canonical: "scoilrince",
|
|
722
|
+
label: "S.R.",
|
|
723
|
+
kind: "word",
|
|
724
|
+
position: "prefix",
|
|
725
|
+
truncatable: false,
|
|
726
|
+
glued: false
|
|
727
|
+
}
|
|
728
|
+
],
|
|
729
|
+
[
|
|
730
|
+
"scoil rince",
|
|
731
|
+
{
|
|
732
|
+
canonical: "scoilrince",
|
|
733
|
+
label: "S.R.",
|
|
734
|
+
kind: "phrase",
|
|
735
|
+
position: "prefix",
|
|
736
|
+
truncatable: false,
|
|
737
|
+
glued: false
|
|
738
|
+
}
|
|
739
|
+
],
|
|
740
|
+
[
|
|
741
|
+
"s r",
|
|
742
|
+
{
|
|
743
|
+
canonical: "scoilrince",
|
|
744
|
+
label: "S.R.",
|
|
745
|
+
kind: "phrase",
|
|
746
|
+
position: "prefix",
|
|
747
|
+
truncatable: false,
|
|
748
|
+
glued: false
|
|
749
|
+
}
|
|
750
|
+
],
|
|
751
|
+
[
|
|
752
|
+
"sr",
|
|
753
|
+
{
|
|
754
|
+
canonical: "scoilrince",
|
|
755
|
+
label: "S.R.",
|
|
756
|
+
kind: "word",
|
|
757
|
+
position: "prefix",
|
|
758
|
+
truncatable: false,
|
|
759
|
+
glued: false
|
|
760
|
+
}
|
|
761
|
+
],
|
|
762
|
+
[
|
|
763
|
+
"studio",
|
|
764
|
+
{
|
|
765
|
+
canonical: "studio",
|
|
766
|
+
label: "Studio",
|
|
767
|
+
kind: "word",
|
|
768
|
+
position: "suffix",
|
|
769
|
+
truncatable: true,
|
|
770
|
+
glued: false
|
|
771
|
+
}
|
|
772
|
+
],
|
|
773
|
+
[
|
|
774
|
+
"studios",
|
|
775
|
+
{
|
|
776
|
+
canonical: "studio",
|
|
777
|
+
label: "Studio",
|
|
778
|
+
kind: "word",
|
|
779
|
+
position: "suffix",
|
|
780
|
+
truncatable: false,
|
|
781
|
+
glued: false
|
|
782
|
+
}
|
|
783
|
+
],
|
|
784
|
+
[
|
|
785
|
+
"company",
|
|
786
|
+
{
|
|
787
|
+
canonical: "company",
|
|
788
|
+
label: "Company",
|
|
789
|
+
kind: "word",
|
|
790
|
+
position: "suffix",
|
|
791
|
+
truncatable: true,
|
|
792
|
+
glued: false
|
|
793
|
+
}
|
|
794
|
+
],
|
|
795
|
+
[
|
|
796
|
+
"institute",
|
|
797
|
+
{
|
|
798
|
+
canonical: "institute",
|
|
799
|
+
label: "Institute",
|
|
800
|
+
kind: "word",
|
|
801
|
+
position: "suffix",
|
|
802
|
+
truncatable: true,
|
|
803
|
+
glued: false
|
|
804
|
+
}
|
|
805
|
+
],
|
|
806
|
+
[
|
|
807
|
+
"center",
|
|
808
|
+
{
|
|
809
|
+
canonical: "center",
|
|
810
|
+
label: "Center",
|
|
811
|
+
kind: "word",
|
|
812
|
+
position: "suffix",
|
|
813
|
+
truncatable: true,
|
|
814
|
+
glued: false
|
|
815
|
+
}
|
|
816
|
+
],
|
|
817
|
+
[
|
|
818
|
+
"centre",
|
|
819
|
+
{
|
|
820
|
+
canonical: "center",
|
|
821
|
+
label: "Center",
|
|
822
|
+
kind: "word",
|
|
823
|
+
position: "suffix",
|
|
824
|
+
truncatable: false,
|
|
825
|
+
glued: false
|
|
826
|
+
}
|
|
827
|
+
],
|
|
828
|
+
[
|
|
829
|
+
"club",
|
|
830
|
+
{
|
|
831
|
+
canonical: "club",
|
|
832
|
+
label: "Club",
|
|
833
|
+
kind: "word",
|
|
834
|
+
position: "suffix",
|
|
835
|
+
truncatable: false,
|
|
836
|
+
glued: false
|
|
837
|
+
}
|
|
838
|
+
]
|
|
839
|
+
]);
|
|
840
|
+
function foldText(text) {
|
|
841
|
+
return text.toLocaleLowerCase().normalize("NFD").replace(/\p{M}/gu, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
842
|
+
}
|
|
843
|
+
function schoolInstitution(text) {
|
|
844
|
+
return SCHOOL_INSTITUTION_VOCABULARY.get(foldText(text));
|
|
845
|
+
}
|
|
846
|
+
var LABEL_BY_TYPE = new Map([...SCHOOL_INSTITUTION_VOCABULARY.values()].map((v) => [v.canonical, v.label]));
|
|
847
|
+
function institutionLabel(type) {
|
|
848
|
+
return LABEL_BY_TYPE.get(type);
|
|
849
|
+
}
|
|
850
|
+
var INSTITUTION_TYPES = [...LABEL_BY_TYPE.keys()].sort();
|
|
851
|
+
var INSTITUTION_PHRASES = [...SCHOOL_INSTITUTION_VOCABULARY].filter(([, v]) => v.kind === "phrase").map(([k]) => k).sort();
|
|
852
|
+
var GLUED_INSTITUTION_SUFFIXES = [...SCHOOL_INSTITUTION_VOCABULARY].filter(([, v]) => v.glued).map(([k]) => k).sort();
|
|
853
|
+
function truncatedInstitution(tail) {
|
|
854
|
+
const folded = foldText(tail);
|
|
855
|
+
if (folded === "")
|
|
856
|
+
return;
|
|
857
|
+
let found;
|
|
858
|
+
for (const [key, entry] of SCHOOL_INSTITUTION_VOCABULARY) {
|
|
859
|
+
if (!entry.truncatable)
|
|
860
|
+
continue;
|
|
861
|
+
if (key.length <= folded.length || !key.startsWith(folded))
|
|
862
|
+
continue;
|
|
863
|
+
if (found !== undefined && found.canonical !== entry.canonical)
|
|
864
|
+
return;
|
|
865
|
+
found ??= entry;
|
|
866
|
+
}
|
|
867
|
+
return found;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
var QUALIFIER_DELIMITER = /[,;:()[\]/–—-]/;
|
|
871
|
+
var NEVER_PLACE = new Map([
|
|
872
|
+
["de", "name particle: `De <Surname>`, and Spanish `de <Place>`"],
|
|
873
|
+
["ni", "Irish patronymic particle: `Ní <Surname>`"],
|
|
874
|
+
["mo", "Irish possessive: `Mo <Word>`"],
|
|
875
|
+
["na", "Irish article: `Rince na <Word>`"],
|
|
876
|
+
["in", "Irish preposition: `<Word> In <Word>`; and `Inc` truncated to `In`"],
|
|
877
|
+
["co", "Company: `<Surname> & Co.`. Bracketed `(US-CO)` is still Colorado"],
|
|
878
|
+
["id", "Irish Dance: `<Surname> School of ID`"],
|
|
879
|
+
["sd", "School of Dance: `<Surname> SD`"]
|
|
880
|
+
]);
|
|
881
|
+
var NEVER_NAME = new Map([
|
|
882
|
+
["de", "name particle: `De <Surname>`; and Spanish `de <Place>`"],
|
|
883
|
+
["ni", "Irish patronymic particle: `Ní <Surname>`"],
|
|
884
|
+
["ui", "Irish patronymic particle: `Uí <Surname>`"],
|
|
885
|
+
["mo", "Irish possessive: `Mo <Word>`"],
|
|
886
|
+
["na", "Irish article: `Rince na <Word>`"],
|
|
887
|
+
["an", "Irish article: `<Word> an Rince`"],
|
|
888
|
+
["in", "Irish preposition: `<Word> In <Word>`; and `Inc` truncated to `In`"],
|
|
889
|
+
["co", "Company: `<Surname> & Co`"],
|
|
890
|
+
["id", "Irish Dance: `ID de <Place>` keys the place, as `Irish Dancing de <Place>` does"],
|
|
891
|
+
["sd", "School of Dance: `<Surname> SD`"],
|
|
892
|
+
["da", "`Dance` truncated: `<Name> Academy of Irish Da` keys the name"]
|
|
893
|
+
]);
|
|
894
|
+
var GENERIC_TLD = "com|net|org|edu|gov|mil|int|info|biz";
|
|
895
|
+
var WEB_SPAN = new RegExp([
|
|
896
|
+
"[A-Za-z][A-Za-z0-9+.-]*://\\S+",
|
|
897
|
+
"(?:^|[\\s([])www\\.\\S+",
|
|
898
|
+
"[^\\s@]+@[^\\s@]+\\.[A-Za-z]{2,}",
|
|
899
|
+
`(?<![A-Za-z0-9.@-])(?:[A-Za-z0-9][A-Za-z0-9-]*\\.)*[A-Za-z0-9][A-Za-z0-9-]+\\.(?:${GENERIC_TLD})(?![A-Za-z0-9-])`
|
|
900
|
+
].join("|"), "giu");
|
|
901
|
+
function printZones(raw) {
|
|
902
|
+
const qualifier = new Set;
|
|
903
|
+
const bracketed = new Set;
|
|
904
|
+
const qualifierAny = new Set;
|
|
905
|
+
const runs = [...raw.matchAll(/[A-Za-z0-9]+/g)];
|
|
906
|
+
const web = [];
|
|
907
|
+
for (const m of raw.matchAll(WEB_SPAN))
|
|
908
|
+
web.push([m.index ?? 0, (m.index ?? 0) + m[0].length]);
|
|
909
|
+
const inside = new Set;
|
|
910
|
+
const outside = new Set;
|
|
911
|
+
const trailing = new Set;
|
|
912
|
+
for (let i = runs.length - 1;i > 0; i--) {
|
|
913
|
+
const here = runs[i];
|
|
914
|
+
const prev = runs[i - 1];
|
|
915
|
+
if (here === undefined || prev === undefined)
|
|
916
|
+
break;
|
|
917
|
+
const gap = raw.slice((prev.index ?? 0) + prev[0].length, here.index ?? 0);
|
|
918
|
+
if (!QUALIFIER_DELIMITER.test(gap))
|
|
919
|
+
break;
|
|
920
|
+
if (i - 1 === 0)
|
|
921
|
+
break;
|
|
922
|
+
trailing.add(i - 1);
|
|
923
|
+
}
|
|
924
|
+
let depth = 0;
|
|
925
|
+
let cursor = 0;
|
|
926
|
+
for (let i = 0;i < runs.length; i++) {
|
|
927
|
+
const run = runs[i];
|
|
928
|
+
const text = run?.[0];
|
|
929
|
+
if (run === undefined || text === undefined)
|
|
930
|
+
continue;
|
|
931
|
+
const at = run.index ?? cursor;
|
|
932
|
+
for (const ch of raw.slice(cursor, at)) {
|
|
933
|
+
if (ch === "(" || ch === "[")
|
|
934
|
+
depth++;
|
|
935
|
+
else if ((ch === ")" || ch === "]") && depth > 0)
|
|
936
|
+
depth--;
|
|
937
|
+
}
|
|
938
|
+
cursor = at + text.length;
|
|
939
|
+
const folded = text.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
940
|
+
(web.some(([s, e]) => at >= s && at < e) ? inside : outside).add(folded);
|
|
941
|
+
const before = raw.slice(0, at).trimEnd().slice(-1);
|
|
942
|
+
const after = raw.slice(cursor).trimStart().slice(0, 1);
|
|
943
|
+
const atQualifier = i === runs.length - 1 || before !== "" && QUALIFIER_DELIMITER.test(before) || after !== "" && QUALIFIER_DELIMITER.test(after) && trailing.has(i);
|
|
944
|
+
if (atQualifier)
|
|
945
|
+
qualifierAny.add(folded);
|
|
946
|
+
if (folded.length > 2)
|
|
947
|
+
continue;
|
|
948
|
+
if (atQualifier)
|
|
949
|
+
qualifier.add(folded);
|
|
950
|
+
if (depth > 0)
|
|
951
|
+
bracketed.add(folded);
|
|
952
|
+
}
|
|
953
|
+
const webOnly = new Set([...inside].filter((f) => !outside.has(f)));
|
|
954
|
+
return { qualifier, bracketed, qualifierAny, web, webOnly, tailCode: tailCode(raw, runs) };
|
|
955
|
+
}
|
|
956
|
+
var GOVERNS_THE_NEXT_WORD = new Set(["de", "mo"]);
|
|
957
|
+
function tailCode(raw, runs) {
|
|
958
|
+
const last = runs[runs.length - 1];
|
|
959
|
+
const prev = runs[runs.length - 2];
|
|
960
|
+
if (last === undefined || prev === undefined)
|
|
961
|
+
return null;
|
|
962
|
+
const text = last[0];
|
|
963
|
+
if (text.length > 2 || text !== text.toUpperCase() || !/^[A-Z]+$/.test(text))
|
|
964
|
+
return null;
|
|
965
|
+
const at = last.index ?? 0;
|
|
966
|
+
const folded = text.toLowerCase();
|
|
967
|
+
const gap = raw.slice((prev.index ?? 0) + prev[0].length, at);
|
|
968
|
+
const marked = /[,;:/\\\u2013\u2014]/u.test(gap) || /\s-|-\s/u.test(gap);
|
|
969
|
+
const bare = GOVERNS_THE_NEXT_WORD.has(folded) && /^\s+$/u.test(gap) && /\p{Ll}/u.test(raw);
|
|
970
|
+
if (!marked && !bare)
|
|
971
|
+
return null;
|
|
972
|
+
const only = runs.filter((r) => r[0].toLowerCase() === folded).length === 1;
|
|
973
|
+
return { folded, at, only };
|
|
974
|
+
}
|
|
975
|
+
function opensLocation(folded, at, index) {
|
|
976
|
+
if (index === undefined ? at.webOnly.has(folded) : inWebSpan(at, index))
|
|
977
|
+
return false;
|
|
978
|
+
if (folded.length > 2)
|
|
979
|
+
return true;
|
|
980
|
+
if (NEVER_PLACE.has(folded))
|
|
981
|
+
return at.bracketed.has(folded) || isTailCode(folded, at, index);
|
|
982
|
+
return at.qualifier.has(folded);
|
|
983
|
+
}
|
|
984
|
+
function isTailCode(folded, at, index) {
|
|
985
|
+
const tail = at.tailCode;
|
|
986
|
+
if (tail === null || tail.folded !== folded)
|
|
987
|
+
return false;
|
|
988
|
+
return index === undefined ? tail.only : index === tail.at;
|
|
989
|
+
}
|
|
990
|
+
function inWebSpan(at, index) {
|
|
991
|
+
return at.web.some(([start, end]) => index >= start && index < end);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
var SCHOOL_FLUFF_WORDS = new Set([
|
|
995
|
+
"academy",
|
|
996
|
+
"irish",
|
|
997
|
+
"dance",
|
|
998
|
+
"dancing",
|
|
999
|
+
"dancers",
|
|
1000
|
+
"school",
|
|
1001
|
+
"company",
|
|
1002
|
+
"traditional",
|
|
1003
|
+
"the",
|
|
1004
|
+
"of",
|
|
1005
|
+
"and",
|
|
1006
|
+
"studio",
|
|
1007
|
+
"center",
|
|
1008
|
+
"centre",
|
|
1009
|
+
"institute",
|
|
1010
|
+
"scoil",
|
|
1011
|
+
"rince",
|
|
1012
|
+
"na",
|
|
1013
|
+
"an"
|
|
1014
|
+
]);
|
|
1015
|
+
var SCHOOL_GENERIC_NAME_PARTS = new Set(["celtic", "clann"]);
|
|
1016
|
+
function hasSpecificToken(tokens) {
|
|
1017
|
+
for (const t of tokens)
|
|
1018
|
+
if (!SCHOOL_GENERIC_NAME_PARTS.has(t))
|
|
1019
|
+
return true;
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
var GLUED_FLUFF_SUFFIXES = ["academy", "school"];
|
|
1023
|
+
function deglueToken(token) {
|
|
1024
|
+
let t = token;
|
|
1025
|
+
let changed = true;
|
|
1026
|
+
while (changed) {
|
|
1027
|
+
changed = false;
|
|
1028
|
+
for (const suf of GLUED_FLUFF_SUFFIXES) {
|
|
1029
|
+
if (t.length > suf.length + 2 && t.endsWith(suf)) {
|
|
1030
|
+
t = t.slice(0, -suf.length);
|
|
1031
|
+
changed = true;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return t;
|
|
1036
|
+
}
|
|
1037
|
+
function isTruncatedFluff2(token) {
|
|
1038
|
+
for (const w of SCHOOL_FLUFF_WORDS) {
|
|
1039
|
+
if (w.length > token.length && w.startsWith(token))
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
function isDiscardedAnyway(token) {
|
|
1045
|
+
return token.length <= 2 || SCHOOL_FLUFF_WORDS.has(token);
|
|
1046
|
+
}
|
|
1047
|
+
function distinctiveTokens(tokens) {
|
|
1048
|
+
return tokens.filter((t) => t.length > 2 && !SCHOOL_FLUFF_WORDS.has(t));
|
|
1049
|
+
}
|
|
1050
|
+
var APOSTROPHE_VARIANTS = /['`´‘’“”]/g;
|
|
1051
|
+
function foldSchoolName(raw) {
|
|
1052
|
+
let s = raw.toLowerCase();
|
|
1053
|
+
s = s.normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
1054
|
+
s = s.replace(/ß/g, "ss").replace(/æ/g, "ae").replace(/œ/g, "oe").replace(/ø/g, "o");
|
|
1055
|
+
s = s.replace(APOSTROPHE_VARIANTS, "'");
|
|
1056
|
+
s = s.replace(/'/g, "");
|
|
1057
|
+
const words = [];
|
|
1058
|
+
for (const word of s.split(/[^a-z0-9]+/)) {
|
|
1059
|
+
if (word === "")
|
|
1060
|
+
continue;
|
|
1061
|
+
const prev = words[words.length - 1];
|
|
1062
|
+
const joined = prev === undefined ? undefined : joinedPrefix(prev, word);
|
|
1063
|
+
if (joined === undefined)
|
|
1064
|
+
words.push(word);
|
|
1065
|
+
else
|
|
1066
|
+
words[words.length - 1] = joined.toLowerCase().replace(/'/g, "");
|
|
1067
|
+
}
|
|
1068
|
+
return words.join(" ");
|
|
1069
|
+
}
|
|
1070
|
+
function schoolKeyTokens(raw) {
|
|
1071
|
+
const folded = foldSchoolName(raw).split(" ").filter((t) => t !== "").map(deglueToken);
|
|
1072
|
+
let end = folded.length;
|
|
1073
|
+
while (end > 0) {
|
|
1074
|
+
const last = folded[end - 1];
|
|
1075
|
+
if (!isTruncatedFluff2(last) && !isDiscardedAnyway(last))
|
|
1076
|
+
break;
|
|
1077
|
+
if (!hasSpecificToken(distinctiveTokens(folded.slice(0, end - 1))))
|
|
1078
|
+
break;
|
|
1079
|
+
end -= 1;
|
|
1080
|
+
}
|
|
1081
|
+
return [...new Set(distinctiveTokens(folded.slice(0, end)))];
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
var SCHOOL_REFUSED = "!!!NOTASCHOOL";
|
|
1085
|
+
var TITLE_CASE_STATE_CODES = new Set(["Nsw", "Qld", "Vic", "Tas"]);
|
|
1086
|
+
function schoolInitials(name) {
|
|
1087
|
+
return INITIALS.get(name) ?? { declared: null, derived: [] };
|
|
1088
|
+
}
|
|
1089
|
+
var TRAILING_BRACKET = /\s*[([]([^()[\]]*)[)\]]\s*$/;
|
|
1090
|
+
var DANGLING_MARK = /[\s,;:/\\\-–—([]+$/;
|
|
1091
|
+
function schoolPrintSplit(print, reader) {
|
|
1092
|
+
const schoolName = (p) => reader.schoolName(p);
|
|
1093
|
+
let parsed = schoolName(print);
|
|
1094
|
+
let annotation = null;
|
|
1095
|
+
if (!LOCATION_AT.has(parsed)) {
|
|
1096
|
+
const bracket = TRAILING_BRACKET.exec(parsed.sn1);
|
|
1097
|
+
const inside = bracket?.[1]?.trim() ?? "";
|
|
1098
|
+
if (bracket !== null && inside !== "") {
|
|
1099
|
+
annotation = inside;
|
|
1100
|
+
parsed = schoolName(parsed.sn1.slice(0, bracket.index));
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const at = LOCATION_AT.get(parsed);
|
|
1104
|
+
const sn1 = parsed.sn1;
|
|
1105
|
+
if (at === undefined)
|
|
1106
|
+
return { name: sn1.trim(), qualifier: null, location: null, annotation };
|
|
1107
|
+
return {
|
|
1108
|
+
name: sn1.slice(0, at).replace(DANGLING_MARK, "").trim(),
|
|
1109
|
+
qualifier: sn1.slice(at).replace(/[\s)\]]+$/, "").trim(),
|
|
1110
|
+
location: parsed.location,
|
|
1111
|
+
annotation
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
var PART_KEYS = new WeakMap;
|
|
1115
|
+
var INITIALS = new WeakMap;
|
|
1116
|
+
var LOCATION_AT = new WeakMap;
|
|
1117
|
+
function realWords(name) {
|
|
1118
|
+
const hit = REAL_WORDS.get(name);
|
|
1119
|
+
if (hit !== undefined)
|
|
1120
|
+
return hit;
|
|
1121
|
+
const words = Object.freeze([...new Set(PART_KEYS.get(name) ?? [])].sort());
|
|
1122
|
+
REAL_WORDS.set(name, words);
|
|
1123
|
+
return words;
|
|
1124
|
+
}
|
|
1125
|
+
function realWordsInPrintOrder(name) {
|
|
1126
|
+
return Object.freeze([...new Set(PART_KEYS.get(name) ?? [])]);
|
|
1127
|
+
}
|
|
1128
|
+
var REAL_WORDS = new WeakMap;
|
|
1129
|
+
var DEFAULT_SCHOOL_NAME_CAPACITY = 16384;
|
|
1130
|
+
function createSchoolNameReader(options = {}) {
|
|
1131
|
+
const lexicon = options.lexicon;
|
|
1132
|
+
const EXCEPTIONS_BY_LENGTH = exceptionsByLength(options.exceptions ?? SCHOOL_NAME_EXCEPTIONS);
|
|
1133
|
+
const notASchool = notASchoolIn([
|
|
1134
|
+
...options.garbage ?? [],
|
|
1135
|
+
...options.refusals ?? NOT_A_SCHOOL
|
|
1136
|
+
]);
|
|
1137
|
+
const CP1252_HIGH = "€‚ƒ„…†‡ˆ‰Š‹Œ" + "Ž‘’“”•–—˜™š›" + "œžŸ";
|
|
1138
|
+
const BYTE_OF_CP1252 = new Map([...CP1252_HIGH].map((ch, i) => [ch.codePointAt(0), 128 + i]));
|
|
1139
|
+
const MOJIBAKE = /[Â-ßà-ï][-¿‚-›ŒŠŽ]/u;
|
|
1140
|
+
function undoMojibake(s) {
|
|
1141
|
+
const bytes = new Uint8Array(s.length);
|
|
1142
|
+
for (let i = 0;i < s.length; i++) {
|
|
1143
|
+
const code = s.charCodeAt(i);
|
|
1144
|
+
const byte = BYTE_OF_CP1252.get(code) ?? code;
|
|
1145
|
+
if (byte > 255)
|
|
1146
|
+
return;
|
|
1147
|
+
bytes[i] = byte;
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function repairMojibake(print) {
|
|
1156
|
+
let s = print;
|
|
1157
|
+
for (let round = 0;round < 3 && MOJIBAKE.test(s); round++) {
|
|
1158
|
+
const undone = undoMojibake(s);
|
|
1159
|
+
if (undone === undefined || undone === s)
|
|
1160
|
+
return s;
|
|
1161
|
+
s = undone;
|
|
1162
|
+
}
|
|
1163
|
+
return s;
|
|
1164
|
+
}
|
|
1165
|
+
function renderSn1(print) {
|
|
1166
|
+
return normalizeApostrophes(repairMojibake(print).normalize("NFKC")).replace(/\p{Cf}+/gu, "").replace(/\p{Cc}+/gu, " ").replace(/\s+/gu, " ").trim();
|
|
1167
|
+
}
|
|
1168
|
+
const COMPOUND_OPENERS = new Set([
|
|
1169
|
+
"hyphen",
|
|
1170
|
+
"slash",
|
|
1171
|
+
"backslash",
|
|
1172
|
+
"comma"
|
|
1173
|
+
]);
|
|
1174
|
+
function separatorOf(gap) {
|
|
1175
|
+
if (gap.includes("-"))
|
|
1176
|
+
return "hyphen";
|
|
1177
|
+
if (gap.includes("/"))
|
|
1178
|
+
return "slash";
|
|
1179
|
+
if (gap.includes("\\"))
|
|
1180
|
+
return "backslash";
|
|
1181
|
+
if (gap.includes(","))
|
|
1182
|
+
return "comma";
|
|
1183
|
+
if (/^\s+$/u.test(gap))
|
|
1184
|
+
return "space";
|
|
1185
|
+
return "other";
|
|
1186
|
+
}
|
|
1187
|
+
function printedWords(sn1) {
|
|
1188
|
+
const out = [];
|
|
1189
|
+
let cursor = 0;
|
|
1190
|
+
for (const match of sn1.matchAll(/[\p{L}\p{N}']+/gu)) {
|
|
1191
|
+
const at = match.index ?? cursor;
|
|
1192
|
+
const gap = sn1.slice(cursor, at);
|
|
1193
|
+
cursor = at + match[0].length;
|
|
1194
|
+
const text = match[0].replace(/^'+|'+$/gu, "");
|
|
1195
|
+
if (text === "")
|
|
1196
|
+
continue;
|
|
1197
|
+
const prev = out[out.length - 1];
|
|
1198
|
+
const joined = prev === undefined ? undefined : joinedPrefix(prev.printed, text);
|
|
1199
|
+
if (prev !== undefined && joined !== undefined) {
|
|
1200
|
+
prev.printed = joined;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
out.push({ printed: text, sep: out.length === 0 ? "start" : separatorOf(gap), at });
|
|
1204
|
+
}
|
|
1205
|
+
return out;
|
|
1206
|
+
}
|
|
1207
|
+
function collapse(tokens, at, len, token) {
|
|
1208
|
+
const words = tokens.slice(at, at + len).flatMap((t) => t.words);
|
|
1209
|
+
tokens.splice(at, len, { ...token, words });
|
|
1210
|
+
}
|
|
1211
|
+
function runAt(tokens, words) {
|
|
1212
|
+
outer:
|
|
1213
|
+
for (let i = 0;i + words.length <= tokens.length; i++) {
|
|
1214
|
+
for (let j = 0;j < words.length; j++) {
|
|
1215
|
+
const t = tokens[i + j];
|
|
1216
|
+
if (t === undefined || t.cls !== null || t.text !== words[j])
|
|
1217
|
+
continue outer;
|
|
1218
|
+
}
|
|
1219
|
+
return i;
|
|
1220
|
+
}
|
|
1221
|
+
return -1;
|
|
1222
|
+
}
|
|
1223
|
+
function applyPhraseExceptions(tokens) {
|
|
1224
|
+
for (;; ) {
|
|
1225
|
+
let hit;
|
|
1226
|
+
let at = -1;
|
|
1227
|
+
for (const ex of EXCEPTIONS_BY_LENGTH) {
|
|
1228
|
+
if (ex.scope !== "phrase")
|
|
1229
|
+
continue;
|
|
1230
|
+
at = runAt(tokens, ex.phrase.split(" "));
|
|
1231
|
+
if (at >= 0) {
|
|
1232
|
+
hit = ex;
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (hit === undefined || at < 0)
|
|
1237
|
+
return;
|
|
1238
|
+
collapse(tokens, at, hit.phrase.split(" ").length, {
|
|
1239
|
+
text: hit.token,
|
|
1240
|
+
cls: hit.as === "NAME_PART" ? "NAME_PART" : "FLUFF",
|
|
1241
|
+
exception: hit.phrase
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
function deglueInstitutions(tokens) {
|
|
1246
|
+
for (let i = 0;i < tokens.length; i++) {
|
|
1247
|
+
const token = tokens[i];
|
|
1248
|
+
if (token === undefined || token.cls !== null)
|
|
1249
|
+
continue;
|
|
1250
|
+
const suffix = GLUED_INSTITUTION_SUFFIXES.find((s) => token.text.length > s.length + 2 && token.text.endsWith(s));
|
|
1251
|
+
if (suffix === undefined)
|
|
1252
|
+
continue;
|
|
1253
|
+
const word = token.words[0];
|
|
1254
|
+
if (token.words.length !== 1 || word.printed.length <= suffix.length)
|
|
1255
|
+
continue;
|
|
1256
|
+
tokens.splice(i, 1, {
|
|
1257
|
+
text: token.text.slice(0, -suffix.length),
|
|
1258
|
+
words: [{ printed: word.printed.slice(0, -suffix.length), sep: word.sep, at: word.at }],
|
|
1259
|
+
cls: null
|
|
1260
|
+
}, {
|
|
1261
|
+
text: suffix,
|
|
1262
|
+
words: [
|
|
1263
|
+
{
|
|
1264
|
+
printed: word.printed.slice(-suffix.length),
|
|
1265
|
+
sep: "other",
|
|
1266
|
+
at: word.at + word.printed.length - suffix.length
|
|
1267
|
+
}
|
|
1268
|
+
],
|
|
1269
|
+
cls: null
|
|
1270
|
+
});
|
|
1271
|
+
i--;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function applyInstitutionPhrases(tokens) {
|
|
1275
|
+
for (const phrase of INSTITUTION_PHRASES) {
|
|
1276
|
+
const words = phrase.split(" ");
|
|
1277
|
+
for (;; ) {
|
|
1278
|
+
const at = runAt(tokens, words);
|
|
1279
|
+
if (at < 0)
|
|
1280
|
+
break;
|
|
1281
|
+
const entry = schoolInstitution(phrase);
|
|
1282
|
+
if (entry === undefined)
|
|
1283
|
+
break;
|
|
1284
|
+
collapse(tokens, at, words.length, {
|
|
1285
|
+
text: phrase,
|
|
1286
|
+
cls: "INSTITUTION_TYPE",
|
|
1287
|
+
institution: entry.canonical
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function applyPlaces(tokens, at, sn1) {
|
|
1293
|
+
for (let i = 1;i < tokens.length; i++) {
|
|
1294
|
+
const token = tokens[i];
|
|
1295
|
+
if (token.cls !== null)
|
|
1296
|
+
continue;
|
|
1297
|
+
if (!opensLocation(token.text, at, token.words[0]?.at))
|
|
1298
|
+
continue;
|
|
1299
|
+
const tail = tokens.slice(i);
|
|
1300
|
+
if (tail.some((t) => t.cls !== null))
|
|
1301
|
+
continue;
|
|
1302
|
+
const words = tail.map((t) => t.words.map((w) => w.printed).join(" "));
|
|
1303
|
+
const gate = codeOutOfCase(tokens, i, sn1);
|
|
1304
|
+
let printed = tail.flatMap((t) => t.words.map((w) => w.printed)).join(" ");
|
|
1305
|
+
if (!commaTailStateCode(tail) && !areAllPlaceTokens2(words, 0, gate)) {
|
|
1306
|
+
const marked = punctuatedTail(sn1, tail);
|
|
1307
|
+
if (marked === null || !areAllPlaceTokens2([marked], 0, gate))
|
|
1308
|
+
continue;
|
|
1309
|
+
printed = marked;
|
|
1310
|
+
}
|
|
1311
|
+
if (surnameBeatsAWholeWord(sn1, tail))
|
|
1312
|
+
continue;
|
|
1313
|
+
const read = place2(printed, { declared: true });
|
|
1314
|
+
if (read.refusal !== null || read.nodes.length === 0)
|
|
1315
|
+
continue;
|
|
1316
|
+
if (surnameBeatsAWholeWord(sn1, tail, read))
|
|
1317
|
+
continue;
|
|
1318
|
+
if (bareCountryCode(token, read))
|
|
1319
|
+
continue;
|
|
1320
|
+
if (read.truncated && nameBeatsACut(printed))
|
|
1321
|
+
continue;
|
|
1322
|
+
if (read.truncated && !survivesWithout(tokens, i, tail.length))
|
|
1323
|
+
continue;
|
|
1324
|
+
for (const t of tail) {
|
|
1325
|
+
t.cls = "LOCATION_HINT";
|
|
1326
|
+
if (read.pl3 !== "")
|
|
1327
|
+
t.place = read.pl3;
|
|
1328
|
+
}
|
|
1329
|
+
return { read, at: token.words[0]?.at ?? 0 };
|
|
1330
|
+
}
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
function punctuatedTail(sn1, tail) {
|
|
1334
|
+
const first = tail[0]?.words[0];
|
|
1335
|
+
const lastToken = tail[tail.length - 1];
|
|
1336
|
+
const last = lastToken?.words[lastToken.words.length - 1];
|
|
1337
|
+
if (first === undefined || last === undefined)
|
|
1338
|
+
return null;
|
|
1339
|
+
const raw = sn1.slice(first.at, last.at + last.printed.length);
|
|
1340
|
+
if (!/[,/;]/.test(raw) || /[()[\]]/.test(raw))
|
|
1341
|
+
return null;
|
|
1342
|
+
return raw;
|
|
1343
|
+
}
|
|
1344
|
+
function codeOutOfCase(tokens, i, sn1) {
|
|
1345
|
+
const opener = tokens[i]?.words[0]?.at;
|
|
1346
|
+
if (opener !== undefined && bracketed(sn1, opener))
|
|
1347
|
+
return true;
|
|
1348
|
+
const before = tokens[tokens.length - 2];
|
|
1349
|
+
if (before === undefined)
|
|
1350
|
+
return false;
|
|
1351
|
+
if (schoolInstitution(before.text) !== undefined)
|
|
1352
|
+
return true;
|
|
1353
|
+
return isPlaceToken2(before.words.map((w) => w.printed).join(" "));
|
|
1354
|
+
}
|
|
1355
|
+
function bracketed(sn1, index) {
|
|
1356
|
+
let start = index;
|
|
1357
|
+
while (start > 0 && !/[\p{L}\p{N}']/u.test(sn1[start - 1]))
|
|
1358
|
+
start--;
|
|
1359
|
+
return /[([]/u.test(sn1.slice(start, index));
|
|
1360
|
+
}
|
|
1361
|
+
function commaTailStateCode(tail) {
|
|
1362
|
+
if (tail.length !== 1)
|
|
1363
|
+
return false;
|
|
1364
|
+
const token = tail[0];
|
|
1365
|
+
if (token.words.length !== 1)
|
|
1366
|
+
return false;
|
|
1367
|
+
const word = token.words[0];
|
|
1368
|
+
return word.sep === "comma" && TITLE_CASE_STATE_CODES.has(word.printed);
|
|
1369
|
+
}
|
|
1370
|
+
function bareCountryCode(token, read) {
|
|
1371
|
+
if (token.words.length !== 1)
|
|
1372
|
+
return false;
|
|
1373
|
+
const word = token.words[0];
|
|
1374
|
+
if (word.sep !== "space")
|
|
1375
|
+
return false;
|
|
1376
|
+
if (word.printed === word.printed.toLocaleUpperCase())
|
|
1377
|
+
return false;
|
|
1378
|
+
const country = countryCodeOf2(word.printed);
|
|
1379
|
+
return country !== undefined && read.nodes.some((n) => n.id === country);
|
|
1380
|
+
}
|
|
1381
|
+
function nameBeatsACut(printed) {
|
|
1382
|
+
if (/\s/.test(printed.trim()))
|
|
1383
|
+
return false;
|
|
1384
|
+
const flags = flagsOf(printed);
|
|
1385
|
+
if (flags === null)
|
|
1386
|
+
return false;
|
|
1387
|
+
return flags.surname || flags.ambiguous || flags.tradename || flags.given;
|
|
1388
|
+
}
|
|
1389
|
+
function surnameBeatsAWholeWord(sn1, tail, read) {
|
|
1390
|
+
if (tail.length !== 1)
|
|
1391
|
+
return false;
|
|
1392
|
+
const token = tail[0];
|
|
1393
|
+
if (token.words.length !== 1)
|
|
1394
|
+
return false;
|
|
1395
|
+
const word = token.words[0];
|
|
1396
|
+
if (word.sep !== "space" && word.sep !== "hyphen")
|
|
1397
|
+
return false;
|
|
1398
|
+
if (declaredQualifier(sn1, word.at))
|
|
1399
|
+
return false;
|
|
1400
|
+
const flags = flagsOf(word.printed);
|
|
1401
|
+
if (flags === null)
|
|
1402
|
+
return false;
|
|
1403
|
+
if (!(flags.surname || flags.ambiguous))
|
|
1404
|
+
return false;
|
|
1405
|
+
return !flags.place || read !== undefined && namesOnlyACountry(read);
|
|
1406
|
+
}
|
|
1407
|
+
function namesOnlyACountry(read) {
|
|
1408
|
+
return read.nodes.some((n) => n.kind === "country") && !read.nodes.some((n) => n.kind === "subdivision" || n.kind === "city");
|
|
1409
|
+
}
|
|
1410
|
+
function applyLexiconPlaces(tokens, at, sn1) {
|
|
1411
|
+
if (lexicon === undefined)
|
|
1412
|
+
return false;
|
|
1413
|
+
let last = tokens.length - 1;
|
|
1414
|
+
while (last >= 0 && tokens[last]?.cls === "LOCATION_HINT")
|
|
1415
|
+
last--;
|
|
1416
|
+
if (last <= 0)
|
|
1417
|
+
return false;
|
|
1418
|
+
const token = tokens[last];
|
|
1419
|
+
if (token.cls !== null)
|
|
1420
|
+
return false;
|
|
1421
|
+
if (!at.qualifierAny.has(token.text))
|
|
1422
|
+
return false;
|
|
1423
|
+
if (!declaredQualifier(sn1, token.words[0]?.at))
|
|
1424
|
+
return false;
|
|
1425
|
+
if (schoolInstitution(token.text) !== undefined)
|
|
1426
|
+
return false;
|
|
1427
|
+
if (truncatedInstitution(token.text) !== undefined)
|
|
1428
|
+
return false;
|
|
1429
|
+
if (isFluffWord(token.text))
|
|
1430
|
+
return false;
|
|
1431
|
+
const flags = flagsOf(token.words.map((w) => w.printed).join(" "));
|
|
1432
|
+
if (flags === null || !flags.place)
|
|
1433
|
+
return false;
|
|
1434
|
+
if (flags.surname || flags.ambiguous || flags.tradename || flags.given)
|
|
1435
|
+
return false;
|
|
1436
|
+
if (!survivesWithout(tokens, last))
|
|
1437
|
+
return false;
|
|
1438
|
+
token.cls = "LOCATION_HINT";
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
function declaredQualifier(sn1, index) {
|
|
1442
|
+
if (index === undefined || index <= 0)
|
|
1443
|
+
return false;
|
|
1444
|
+
let start = index;
|
|
1445
|
+
while (start > 0 && !/[\p{L}\p{N}']/u.test(sn1[start - 1]))
|
|
1446
|
+
start--;
|
|
1447
|
+
const gap = sn1.slice(start, index);
|
|
1448
|
+
if (/[,;:/\\([\u2013\u2014]/u.test(gap))
|
|
1449
|
+
return true;
|
|
1450
|
+
return /\s-|-\s/u.test(gap);
|
|
1451
|
+
}
|
|
1452
|
+
function distinctive(text) {
|
|
1453
|
+
if (text.length <= 2)
|
|
1454
|
+
return false;
|
|
1455
|
+
if (schoolInstitution(text) !== undefined)
|
|
1456
|
+
return false;
|
|
1457
|
+
if (truncatedInstitution(text) !== undefined)
|
|
1458
|
+
return false;
|
|
1459
|
+
return !isFluffWord(text);
|
|
1460
|
+
}
|
|
1461
|
+
function survivesWithout(tokens, at, width = 1) {
|
|
1462
|
+
const off = (i) => i >= at && i < at + width;
|
|
1463
|
+
if (tokens.some((t, i) => !off(i) && (t.cls === "NAME_PART" || t.cls === null && distinctive(t.text))))
|
|
1464
|
+
return true;
|
|
1465
|
+
const core = tokens.filter((t, i) => !off(i) && t.cls === null).map((t) => t.text).join(" ");
|
|
1466
|
+
if (core === "")
|
|
1467
|
+
return false;
|
|
1468
|
+
return EXCEPTIONS_BY_LENGTH.some((ex) => ex.scope === "label" && ex.phrase === core);
|
|
1469
|
+
}
|
|
1470
|
+
function applyLabelExceptions(tokens) {
|
|
1471
|
+
if (tokens.some((t) => t.cls === "NAME_PART"))
|
|
1472
|
+
return;
|
|
1473
|
+
const core = tokens.filter((t) => t.cls === null).map((t) => t.text).join(" ");
|
|
1474
|
+
for (const ex of EXCEPTIONS_BY_LENGTH) {
|
|
1475
|
+
if (ex.scope !== "label" || core !== ex.phrase)
|
|
1476
|
+
continue;
|
|
1477
|
+
const first = tokens.findIndex((t) => t.cls === null);
|
|
1478
|
+
const words = tokens.filter((t) => t.cls === null).flatMap((t) => t.words);
|
|
1479
|
+
if (first < 0 || words.length === 0)
|
|
1480
|
+
return;
|
|
1481
|
+
for (let i = tokens.length - 1;i >= 0; i--)
|
|
1482
|
+
if (tokens[i]?.cls === null)
|
|
1483
|
+
tokens.splice(i, 1);
|
|
1484
|
+
tokens.splice(first, 0, {
|
|
1485
|
+
text: ex.token,
|
|
1486
|
+
words,
|
|
1487
|
+
cls: ex.as === "NAME_PART" ? "NAME_PART" : "FLUFF",
|
|
1488
|
+
exception: ex.phrase
|
|
1489
|
+
});
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function classifyWords(tokens) {
|
|
1494
|
+
for (const token of tokens) {
|
|
1495
|
+
if (token.cls !== null)
|
|
1496
|
+
continue;
|
|
1497
|
+
const inst = schoolInstitution(token.text);
|
|
1498
|
+
if (inst !== undefined) {
|
|
1499
|
+
token.cls = "INSTITUTION_TYPE";
|
|
1500
|
+
token.institution = inst.canonical;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (isFluffWord(token.text)) {
|
|
1504
|
+
token.cls = "FLUFF";
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (token.text.length <= 2) {
|
|
1508
|
+
token.cls = "FLUFF";
|
|
1509
|
+
token.short = true;
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
token.cls = "NAME_PART";
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function guardPrefixInstitutions(tokens) {
|
|
1516
|
+
for (let i = 0;i < tokens.length; i++) {
|
|
1517
|
+
const token = tokens[i];
|
|
1518
|
+
if (token === undefined || token.cls !== "INSTITUTION_TYPE")
|
|
1519
|
+
continue;
|
|
1520
|
+
if (schoolInstitution(token.text)?.position !== "prefix")
|
|
1521
|
+
continue;
|
|
1522
|
+
if (tokens.slice(i + 1).some((t) => t.cls === "NAME_PART"))
|
|
1523
|
+
continue;
|
|
1524
|
+
token.institution = undefined;
|
|
1525
|
+
if (isFluffWord(token.text))
|
|
1526
|
+
token.cls = "FLUFF";
|
|
1527
|
+
else if (token.text.length <= 2) {
|
|
1528
|
+
token.cls = "FLUFF";
|
|
1529
|
+
token.short = true;
|
|
1530
|
+
} else
|
|
1531
|
+
token.cls = "NAME_PART";
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function eatTruncatedTail(tokens) {
|
|
1535
|
+
let ate = false;
|
|
1536
|
+
const names = new Set(tokens.filter((t) => t.cls === "NAME_PART").map((t) => t.text));
|
|
1537
|
+
const start = trailingFluffRun(tokens.map((t) => t.text), (text) => names.has(text));
|
|
1538
|
+
for (let i = start;i < tokens.length; i++) {
|
|
1539
|
+
const token = tokens[i];
|
|
1540
|
+
if (token === undefined || token.cls !== "NAME_PART")
|
|
1541
|
+
continue;
|
|
1542
|
+
const entry = truncatedInstitution(token.text);
|
|
1543
|
+
if (entry !== undefined) {
|
|
1544
|
+
token.cls = "INSTITUTION_TYPE";
|
|
1545
|
+
token.institution = entry.canonical;
|
|
1546
|
+
} else
|
|
1547
|
+
token.cls = "FLUFF";
|
|
1548
|
+
token.truncated = true;
|
|
1549
|
+
ate = true;
|
|
1550
|
+
}
|
|
1551
|
+
for (let end = tokens.length;end > 0; end--) {
|
|
1552
|
+
const token = tokens[end - 1];
|
|
1553
|
+
if (token === undefined)
|
|
1554
|
+
break;
|
|
1555
|
+
if (token.cls === "FLUFF")
|
|
1556
|
+
continue;
|
|
1557
|
+
if (token.cls !== "NAME_PART")
|
|
1558
|
+
break;
|
|
1559
|
+
const entry = truncatedInstitution(token.text);
|
|
1560
|
+
if (entry === undefined)
|
|
1561
|
+
break;
|
|
1562
|
+
if (!tokens.slice(0, end - 1).some((t) => t.cls === "NAME_PART"))
|
|
1563
|
+
break;
|
|
1564
|
+
token.cls = "INSTITUTION_TYPE";
|
|
1565
|
+
token.institution = entry.canonical;
|
|
1566
|
+
token.truncated = true;
|
|
1567
|
+
ate = true;
|
|
1568
|
+
}
|
|
1569
|
+
return ate;
|
|
1570
|
+
}
|
|
1571
|
+
const CONNECTORS = new Set(["of", "and", "the", "na", "an", "a", "do", "de", "du", "la", "le"]);
|
|
1572
|
+
function initialsOfWords(words) {
|
|
1573
|
+
const folded = words.map((w) => foldSchoolName(w.printed)).filter((w) => /^[a-z]/.test(w));
|
|
1574
|
+
const all = folded.map((w) => w[0]).join("");
|
|
1575
|
+
const named = folded.filter((w) => !CONNECTORS.has(w)).map((w) => w[0]).join("");
|
|
1576
|
+
return [...new Set([all, named])].filter((i) => i.length >= 3).map((i) => i.toUpperCase());
|
|
1577
|
+
}
|
|
1578
|
+
function applyDeclaredInitialism(tokens) {
|
|
1579
|
+
for (let i = 1;i < tokens.length; i++) {
|
|
1580
|
+
const token = tokens[i];
|
|
1581
|
+
if (token === undefined || token.cls !== "NAME_PART" || token.exception !== undefined)
|
|
1582
|
+
continue;
|
|
1583
|
+
if (!/^[a-z]{3,8}$/.test(token.text))
|
|
1584
|
+
continue;
|
|
1585
|
+
const before = tokens.slice(0, i);
|
|
1586
|
+
if (!before.some((t) => t.cls === "NAME_PART"))
|
|
1587
|
+
continue;
|
|
1588
|
+
const spelled = initialsOfWords(before.flatMap((t) => t.words));
|
|
1589
|
+
if (!spelled.includes(token.text.toUpperCase()))
|
|
1590
|
+
continue;
|
|
1591
|
+
token.cls = "FLUFF";
|
|
1592
|
+
return token.text.toUpperCase();
|
|
1593
|
+
}
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
function initialsOf(tokens, declared) {
|
|
1597
|
+
const words = tokens.filter((t) => t.cls !== "LOCATION_HINT" && t.text.toUpperCase() !== declared).flatMap((t) => t.words);
|
|
1598
|
+
return Object.freeze({ declared, derived: Object.freeze(initialsOfWords(words)) });
|
|
1599
|
+
}
|
|
1600
|
+
function promote(tokens) {
|
|
1601
|
+
if (tokens.some((t) => t.cls === "NAME_PART"))
|
|
1602
|
+
return false;
|
|
1603
|
+
const isInitials = (t) => t.short === true && !NEVER_NAME.has(t.text);
|
|
1604
|
+
const initials = tokens.some(isInitials);
|
|
1605
|
+
let promoted = false;
|
|
1606
|
+
for (const token of tokens) {
|
|
1607
|
+
if (initials ? !isInitials(token) : token.cls !== "LOCATION_HINT")
|
|
1608
|
+
continue;
|
|
1609
|
+
token.cls = "NAME_PART";
|
|
1610
|
+
token.promoted = true;
|
|
1611
|
+
promoted = true;
|
|
1612
|
+
}
|
|
1613
|
+
return promoted;
|
|
1614
|
+
}
|
|
1615
|
+
function joinerOf(sep) {
|
|
1616
|
+
switch (sep) {
|
|
1617
|
+
case "hyphen":
|
|
1618
|
+
return "-";
|
|
1619
|
+
case "slash":
|
|
1620
|
+
return "/";
|
|
1621
|
+
case "backslash":
|
|
1622
|
+
return "\\";
|
|
1623
|
+
case "comma":
|
|
1624
|
+
return ", ";
|
|
1625
|
+
default:
|
|
1626
|
+
return " ";
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function flagsOf(printed) {
|
|
1630
|
+
if (lexicon === undefined)
|
|
1631
|
+
return null;
|
|
1632
|
+
const key = lexiconKey(printed);
|
|
1633
|
+
return key === "" ? null : lexicon(key) ?? null;
|
|
1634
|
+
}
|
|
1635
|
+
const EXCEPTION_PHRASE = new Map([...EXCEPTIONS_BY_LENGTH].map((ex) => [ex.token, ex.phrase]));
|
|
1636
|
+
function solidKey(word) {
|
|
1637
|
+
return word.replace(/[^a-z0-9]+/g, "").toLocaleUpperCase();
|
|
1638
|
+
}
|
|
1639
|
+
function tokenKey(token) {
|
|
1640
|
+
const phrase = token.exception === undefined ? undefined : EXCEPTION_PHRASE.get(token.text);
|
|
1641
|
+
if (phrase === undefined)
|
|
1642
|
+
return solidKey(token.text);
|
|
1643
|
+
return phrase.split(" ").map(solidKey).filter((w) => w !== "").join(" ");
|
|
1644
|
+
}
|
|
1645
|
+
function partsOf(tokens) {
|
|
1646
|
+
const out = [];
|
|
1647
|
+
let run = [];
|
|
1648
|
+
const printedOf = (t) => t.words.map((w) => w.printed).join(" ");
|
|
1649
|
+
const flush = () => {
|
|
1650
|
+
if (run.length === 0)
|
|
1651
|
+
return;
|
|
1652
|
+
const members = run;
|
|
1653
|
+
run = [];
|
|
1654
|
+
if (members.length === 1) {
|
|
1655
|
+
const only = members[0];
|
|
1656
|
+
const base = printedOf(only);
|
|
1657
|
+
out.push({
|
|
1658
|
+
part: { base, parts: [], lexicon: flagsOf(base) },
|
|
1659
|
+
key: tokenKey(only),
|
|
1660
|
+
members: [tokenKey(only)],
|
|
1661
|
+
tokens: members
|
|
1662
|
+
});
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const base = members.map((t, i) => i === 0 ? printedOf(t) : joinerOf(t.words[0].sep) + printedOf(t)).join("");
|
|
1666
|
+
out.push({
|
|
1667
|
+
part: {
|
|
1668
|
+
base,
|
|
1669
|
+
parts: members.map((t) => ({
|
|
1670
|
+
base: printedOf(t),
|
|
1671
|
+
parts: [],
|
|
1672
|
+
lexicon: flagsOf(printedOf(t))
|
|
1673
|
+
})),
|
|
1674
|
+
lexicon: flagsOf(base)
|
|
1675
|
+
},
|
|
1676
|
+
key: members.map(tokenKey).join(" "),
|
|
1677
|
+
members: members.map(tokenKey),
|
|
1678
|
+
tokens: members
|
|
1679
|
+
});
|
|
1680
|
+
};
|
|
1681
|
+
for (const token of tokens) {
|
|
1682
|
+
if (token.cls !== "NAME_PART") {
|
|
1683
|
+
flush();
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
if (run.length > 0 && !COMPOUND_OPENERS.has(token.words[0].sep))
|
|
1687
|
+
flush();
|
|
1688
|
+
run.push(token);
|
|
1689
|
+
}
|
|
1690
|
+
flush();
|
|
1691
|
+
return out;
|
|
1692
|
+
}
|
|
1693
|
+
function locationKey(read) {
|
|
1694
|
+
if (read === null)
|
|
1695
|
+
return "";
|
|
1696
|
+
if (read.pl3 !== "")
|
|
1697
|
+
return read.pl3;
|
|
1698
|
+
if (!read.isAmbiguous())
|
|
1699
|
+
return "";
|
|
1700
|
+
const ids = read.arm === "ambiguous" ? read.candidates.map((c) => c.pl3).filter((id) => id !== "") : [];
|
|
1701
|
+
if (ids.length === 0)
|
|
1702
|
+
return "";
|
|
1703
|
+
return `${[...ids].sort().join("|")}?`;
|
|
1704
|
+
}
|
|
1705
|
+
function renderKey(keys, institution, read, ordered) {
|
|
1706
|
+
if (keys.length === 0)
|
|
1707
|
+
return "";
|
|
1708
|
+
const core = (ordered ? [...keys] : [...keys].sort()).join(" ");
|
|
1709
|
+
const type = institution === null ? "" : ` ${institution.toLocaleUpperCase()}`;
|
|
1710
|
+
const at = locationKey(read);
|
|
1711
|
+
return `${core}${type}${at === "" ? "" : ` (${at})`}`;
|
|
1712
|
+
}
|
|
1713
|
+
function nameWord(printed, first) {
|
|
1714
|
+
const folded = foldSchoolName(printed);
|
|
1715
|
+
if (!first && schoolFluff(folded)?.lowerInside === true && printed !== printed.toLocaleUpperCase())
|
|
1716
|
+
return printed.toLocaleLowerCase();
|
|
1717
|
+
return titleCaseNamePart(printed);
|
|
1718
|
+
}
|
|
1719
|
+
function renderException(token, first) {
|
|
1720
|
+
const printed = token.words.map((w) => w.printed).join(" ");
|
|
1721
|
+
const canonical = EXCEPTION_PHRASE.get(token.text);
|
|
1722
|
+
const text = canonical !== undefined && foldSchoolName(printed) !== canonical ? canonical : printed;
|
|
1723
|
+
return text.split(" ").map((w, i) => nameWord(w, first && i === 0)).join(" ");
|
|
1724
|
+
}
|
|
1725
|
+
function renderPart(part, first) {
|
|
1726
|
+
const only = part.tokens[0];
|
|
1727
|
+
if (part.tokens.length === 1 && only !== undefined && only.exception !== undefined)
|
|
1728
|
+
return renderException(only, first);
|
|
1729
|
+
const members = part.part.parts.length === 0 ? [part.part.base] : part.tokens.map((t) => ({ key: tokenKey(t), printed: t.words.map((w) => w.printed).join(" ") })).sort((a, b) => a.key < b.key ? -1 : 1).map((m) => m.printed);
|
|
1730
|
+
return members.map((m, i) => m.split(" ").map((w, j) => nameWord(w, first && i === 0 && j === 0)).join(" ")).join(" ");
|
|
1731
|
+
}
|
|
1732
|
+
function renderSn2(tokens, parts, institution, read) {
|
|
1733
|
+
const at = tokens.map((t) => t.cls);
|
|
1734
|
+
const last = at.lastIndexOf("NAME_PART");
|
|
1735
|
+
let first = at.indexOf("NAME_PART");
|
|
1736
|
+
const joins = (t) => t?.cls === "FLUFF" && t.truncated !== true && schoolFluff(t.text)?.descriptor !== true;
|
|
1737
|
+
while (first > 0 && joins(tokens[first - 1]))
|
|
1738
|
+
first--;
|
|
1739
|
+
const pieces = [];
|
|
1740
|
+
let partAt = 0;
|
|
1741
|
+
let started = false;
|
|
1742
|
+
for (let i = first;i <= last && first >= 0; i++) {
|
|
1743
|
+
const token = tokens[i];
|
|
1744
|
+
if (token === undefined)
|
|
1745
|
+
continue;
|
|
1746
|
+
if (token.cls === "NAME_PART") {
|
|
1747
|
+
const inRun = tokens[i - 1]?.cls === "NAME_PART" && COMPOUND_OPENERS.has(token.words[0].sep);
|
|
1748
|
+
if (inRun)
|
|
1749
|
+
continue;
|
|
1750
|
+
const part = parts[partAt];
|
|
1751
|
+
partAt++;
|
|
1752
|
+
if (part !== undefined) {
|
|
1753
|
+
pieces.push(renderPart(part, !started));
|
|
1754
|
+
started = true;
|
|
1755
|
+
}
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
if (joins(token))
|
|
1759
|
+
pieces.push(token.words.map((w) => nameWord(w.printed, !started)).join(" "));
|
|
1760
|
+
}
|
|
1761
|
+
const core = pieces.join(" ");
|
|
1762
|
+
const shown = read === null ? "" : read.pl2;
|
|
1763
|
+
const tail = shown === "" ? "" : ` (${shown})`;
|
|
1764
|
+
if (institution === null)
|
|
1765
|
+
return `${core}${tail}`;
|
|
1766
|
+
if (institution === "scoilrince")
|
|
1767
|
+
return `${institutionLabel("scoilrince")} ${core}${tail}`.trim();
|
|
1768
|
+
return `${core === "" ? "" : `${core} `}${institutionLabel(institution)}${tail}`;
|
|
1769
|
+
}
|
|
1770
|
+
function materialize(b) {
|
|
1771
|
+
const out = {
|
|
1772
|
+
get [Symbol.toStringTag]() {
|
|
1773
|
+
return "SchoolName";
|
|
1774
|
+
},
|
|
1775
|
+
toString: () => b.sn2,
|
|
1776
|
+
sn0: b.sn0,
|
|
1777
|
+
sn1: b.sn1,
|
|
1778
|
+
sn2: b.sn2,
|
|
1779
|
+
sn3: b.sn3,
|
|
1780
|
+
sn4: b.sn4,
|
|
1781
|
+
parts: b.parts,
|
|
1782
|
+
institution: b.institution,
|
|
1783
|
+
location: b.location,
|
|
1784
|
+
refusal: b.refusal,
|
|
1785
|
+
isEmpty: () => b.empty,
|
|
1786
|
+
isSchool: () => !b.empty && b.refusal === null,
|
|
1787
|
+
surnames: () => b.surnames,
|
|
1788
|
+
concerns: b.concerns
|
|
1789
|
+
};
|
|
1790
|
+
PART_KEYS.set(out, b.keys);
|
|
1791
|
+
if (b.initials !== undefined)
|
|
1792
|
+
INITIALS.set(out, b.initials);
|
|
1793
|
+
if (b.locationAt !== undefined)
|
|
1794
|
+
LOCATION_AT.set(out, b.locationAt);
|
|
1795
|
+
return out;
|
|
1796
|
+
}
|
|
1797
|
+
const EMPTY = {
|
|
1798
|
+
sn2: "",
|
|
1799
|
+
sn3: "",
|
|
1800
|
+
sn4: "",
|
|
1801
|
+
parts: [],
|
|
1802
|
+
keys: [],
|
|
1803
|
+
institution: null,
|
|
1804
|
+
location: null,
|
|
1805
|
+
refusal: null,
|
|
1806
|
+
empty: true,
|
|
1807
|
+
surnames: [],
|
|
1808
|
+
concerns: []
|
|
1809
|
+
};
|
|
1810
|
+
function surnamesOf(parts) {
|
|
1811
|
+
const out = new Set;
|
|
1812
|
+
const visit = (part) => {
|
|
1813
|
+
if (part.parts.length > 0) {
|
|
1814
|
+
for (const child of part.parts)
|
|
1815
|
+
visit(child);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const flags = part.lexicon;
|
|
1819
|
+
if (flags === null || !flags.surname && !flags.ambiguous)
|
|
1820
|
+
return;
|
|
1821
|
+
const key = personName(`${part.base}${SURNAME_SEPARATOR}`).surnameKey();
|
|
1822
|
+
if (key !== "")
|
|
1823
|
+
out.add(key);
|
|
1824
|
+
};
|
|
1825
|
+
for (const part of parts)
|
|
1826
|
+
visit(part);
|
|
1827
|
+
return [...out].sort();
|
|
1828
|
+
}
|
|
1829
|
+
function build(print) {
|
|
1830
|
+
const sn1 = renderSn1(print);
|
|
1831
|
+
if (!/[\p{L}\p{N}]/u.test(sn1))
|
|
1832
|
+
return materialize({ ...EMPTY, sn0: print, sn1 });
|
|
1833
|
+
const row = notASchool(sn1);
|
|
1834
|
+
if (row !== undefined) {
|
|
1835
|
+
return materialize({
|
|
1836
|
+
...EMPTY,
|
|
1837
|
+
sn0: print,
|
|
1838
|
+
sn1,
|
|
1839
|
+
sn2: SCHOOL_REFUSED,
|
|
1840
|
+
sn3: SCHOOL_REFUSED,
|
|
1841
|
+
sn4: SCHOOL_REFUSED,
|
|
1842
|
+
empty: false,
|
|
1843
|
+
refusal: { reason: row.reason, printed: print, row, value: SCHOOL_REFUSED }
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
const tokens = [];
|
|
1847
|
+
for (const word of printedWords(sn1)) {
|
|
1848
|
+
const text = foldSchoolName(word.printed);
|
|
1849
|
+
if (text === "")
|
|
1850
|
+
continue;
|
|
1851
|
+
tokens.push({ text, words: [word], cls: null });
|
|
1852
|
+
}
|
|
1853
|
+
if (tokens.length === 0)
|
|
1854
|
+
return materialize({ ...EMPTY, sn0: print, sn1 });
|
|
1855
|
+
deglueInstitutions(tokens);
|
|
1856
|
+
applyPhraseExceptions(tokens);
|
|
1857
|
+
applyInstitutionPhrases(tokens);
|
|
1858
|
+
const positions = printZones(sn1);
|
|
1859
|
+
const tail = applyPlaces(tokens, positions, sn1);
|
|
1860
|
+
const read = tail?.read ?? null;
|
|
1861
|
+
const cutPlace = read?.truncated === true;
|
|
1862
|
+
const lexiconPlace = applyLexiconPlaces(tokens, positions, sn1);
|
|
1863
|
+
applyLabelExceptions(tokens);
|
|
1864
|
+
classifyWords(tokens);
|
|
1865
|
+
guardPrefixInstitutions(tokens);
|
|
1866
|
+
const declared = applyDeclaredInitialism(tokens);
|
|
1867
|
+
const truncated = eatTruncatedTail(tokens);
|
|
1868
|
+
const promoted = promote(tokens);
|
|
1869
|
+
const built = partsOf(tokens);
|
|
1870
|
+
const parts = built.map((p) => p.part);
|
|
1871
|
+
const keys = built.map((p) => p.key);
|
|
1872
|
+
const members = built.flatMap((p) => p.members);
|
|
1873
|
+
const institution = tokens.find((t) => t.institution !== undefined)?.institution ?? null;
|
|
1874
|
+
const named = promoted && tokens.some((t) => t.promoted === true && t.place !== undefined);
|
|
1875
|
+
const location = read === null || named ? null : read;
|
|
1876
|
+
const carried = read !== null && named && declaredQualifier(sn1, tail?.at) ? read : location;
|
|
1877
|
+
const concerns = [];
|
|
1878
|
+
const first = tokens.findIndex((t) => t.cls === "NAME_PART");
|
|
1879
|
+
const last = tokens.map((t) => t.cls).lastIndexOf("NAME_PART");
|
|
1880
|
+
if (tokens.some((t, i) => t.cls === "INSTITUTION_TYPE" && i > first && i < last))
|
|
1881
|
+
concerns.push("institution-inside-name");
|
|
1882
|
+
if (promoted)
|
|
1883
|
+
concerns.push("promoted-name");
|
|
1884
|
+
if (parts.length === 0)
|
|
1885
|
+
concerns.push("no-name-part");
|
|
1886
|
+
if (truncated || cutPlace)
|
|
1887
|
+
concerns.push("truncated-tail");
|
|
1888
|
+
if (lexicon === undefined)
|
|
1889
|
+
concerns.push("lexicon-absent");
|
|
1890
|
+
if (lexiconPlace)
|
|
1891
|
+
concerns.push("place-from-lexicon");
|
|
1892
|
+
if (parts.some((p) => p.lexicon?.ambiguous === true || p.parts.some((c) => c.lexicon?.ambiguous)))
|
|
1893
|
+
concerns.push("ambiguous-part");
|
|
1894
|
+
return materialize({
|
|
1895
|
+
sn0: print,
|
|
1896
|
+
sn1,
|
|
1897
|
+
sn2: renderSn2(tokens, built, institution, location),
|
|
1898
|
+
sn3: renderKey(keys, institution, location, true),
|
|
1899
|
+
sn4: renderKey(members, institution, location, false),
|
|
1900
|
+
parts,
|
|
1901
|
+
keys: members,
|
|
1902
|
+
institution,
|
|
1903
|
+
location: carried,
|
|
1904
|
+
refusal: null,
|
|
1905
|
+
empty: false,
|
|
1906
|
+
surnames: surnamesOf(parts),
|
|
1907
|
+
concerns,
|
|
1908
|
+
initials: initialsOf(tokens, declared),
|
|
1909
|
+
...location === null || tail === null ? {} : { locationAt: tail.at }
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
let cache = new Map;
|
|
1913
|
+
let capacity = options.capacity ?? DEFAULT_SCHOOL_NAME_CAPACITY;
|
|
1914
|
+
let hits = 0;
|
|
1915
|
+
let misses = 0;
|
|
1916
|
+
let evictions = 0;
|
|
1917
|
+
function schoolName(print) {
|
|
1918
|
+
const hit = cache.get(print);
|
|
1919
|
+
if (hit !== undefined) {
|
|
1920
|
+
hits++;
|
|
1921
|
+
return hit;
|
|
1922
|
+
}
|
|
1923
|
+
misses++;
|
|
1924
|
+
const built = build(print);
|
|
1925
|
+
if (capacity > 0) {
|
|
1926
|
+
if (cache.size >= capacity) {
|
|
1927
|
+
const oldest = cache.keys().next();
|
|
1928
|
+
if (!oldest.done) {
|
|
1929
|
+
cache.delete(oldest.value);
|
|
1930
|
+
evictions++;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
cache.set(print, built);
|
|
1934
|
+
}
|
|
1935
|
+
return built;
|
|
1936
|
+
}
|
|
1937
|
+
function schoolNameStats() {
|
|
1938
|
+
return { hits, misses, evictions };
|
|
1939
|
+
}
|
|
1940
|
+
function resetSchoolNameCache(next) {
|
|
1941
|
+
cache = new Map;
|
|
1942
|
+
hits = 0;
|
|
1943
|
+
misses = 0;
|
|
1944
|
+
evictions = 0;
|
|
1945
|
+
if (next !== undefined)
|
|
1946
|
+
capacity = next;
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
lexicon,
|
|
1950
|
+
schoolName,
|
|
1951
|
+
schoolNameStats,
|
|
1952
|
+
resetSchoolNameCache,
|
|
1953
|
+
capacity: () => capacity
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
var DEFAULT_SCHOOL_NAME_READER = Object.freeze(createSchoolNameReader());
|
|
1957
|
+
var LADDER = new Set(["sn0", "sn1", "sn2", "sn3", "sn4"]);
|
|
1958
|
+
function schoolTokensAt(name, dop) {
|
|
1959
|
+
if (dop === "realWords")
|
|
1960
|
+
return realWords(name);
|
|
1961
|
+
if (dop === "surnames")
|
|
1962
|
+
return name.surnames();
|
|
1963
|
+
return [name[dop]];
|
|
1964
|
+
}
|
|
1965
|
+
var UNIT = String.fromCharCode(31);
|
|
1966
|
+
function keyAt(name, dop) {
|
|
1967
|
+
return schoolTokensAt(name, dop).join(UNIT);
|
|
1968
|
+
}
|
|
1969
|
+
function comparable(name) {
|
|
1970
|
+
return name.isSchool() && !name.isEmpty();
|
|
1971
|
+
}
|
|
1972
|
+
function keyTokens(name, dop) {
|
|
1973
|
+
if (!comparable(name))
|
|
1974
|
+
return null;
|
|
1975
|
+
const tokens = schoolTokensAt(name, dop).filter((t) => t !== "");
|
|
1976
|
+
return tokens.length > 0 ? tokens : null;
|
|
1977
|
+
}
|
|
1978
|
+
function requireLexicon(dop, ...names) {
|
|
1979
|
+
if (dop !== "surnames")
|
|
1980
|
+
return;
|
|
1981
|
+
if (!names.some((n) => n.concerns.includes("lexicon-absent")))
|
|
1982
|
+
return;
|
|
1983
|
+
throw new Error('probablySameSchool at "surnames" needs a lexicon: read the schools with one ' + "(createSchoolNameReader({ lexicon })). " + "Without one every print answers [] and every pair would compare equal.");
|
|
1984
|
+
}
|
|
1985
|
+
function probablySameSchool(a, b, dop) {
|
|
1986
|
+
requireLexicon(dop, a, b);
|
|
1987
|
+
const x = keyTokens(a, dop);
|
|
1988
|
+
const y = keyTokens(b, dop);
|
|
1989
|
+
if (x === null || y === null || x.length !== y.length)
|
|
1990
|
+
return false;
|
|
1991
|
+
for (let i = 0;i < x.length; i++)
|
|
1992
|
+
if (x[i] !== y[i])
|
|
1993
|
+
return false;
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
var LONG_GENERICS = [
|
|
1997
|
+
...[...SCHOOL_INSTITUTION_VOCABULARY].filter(([, v]) => v.kind === "word").map(([k]) => k),
|
|
1998
|
+
...[...SCHOOL_FLUFF_VOCABULARY.values()].filter((v) => v.kind === "word").map((v) => v.canonical)
|
|
1999
|
+
].filter((w) => w.length >= 6);
|
|
2000
|
+
function oneEditApart(x, y) {
|
|
2001
|
+
if (x === y || Math.abs(x.length - y.length) > 1)
|
|
2002
|
+
return false;
|
|
2003
|
+
let i = 0;
|
|
2004
|
+
while (i < x.length && i < y.length && x[i] === y[i])
|
|
2005
|
+
i++;
|
|
2006
|
+
if (x.length === y.length) {
|
|
2007
|
+
if (x.slice(i + 1) === y.slice(i + 1))
|
|
2008
|
+
return true;
|
|
2009
|
+
return x[i] === y[i + 1] && x[i + 1] === y[i] && x.slice(i + 2) === y.slice(i + 2);
|
|
2010
|
+
}
|
|
2011
|
+
return x.length > y.length ? x.slice(i + 1) === y.slice(i) : x.slice(i) === y.slice(i + 1);
|
|
2012
|
+
}
|
|
2013
|
+
function damagedGeneric(token) {
|
|
2014
|
+
const t = token.toLocaleLowerCase();
|
|
2015
|
+
return LONG_GENERICS.some((w) => oneEditApart(t, w));
|
|
2016
|
+
}
|
|
2017
|
+
function differOnlyByDamagedGenerics(a, b) {
|
|
2018
|
+
if (!comparable(a) || !comparable(b))
|
|
2019
|
+
return false;
|
|
2020
|
+
const x = new Set(realWords(a));
|
|
2021
|
+
const y = new Set(realWords(b));
|
|
2022
|
+
const shared = [...x].filter((t) => y.has(t));
|
|
2023
|
+
if (shared.length === 0)
|
|
2024
|
+
return false;
|
|
2025
|
+
const extra = [...x, ...y].filter((t) => !(x.has(t) && y.has(t)));
|
|
2026
|
+
return extra.length > 0 && extra.every(damagedGeneric);
|
|
2027
|
+
}
|
|
2028
|
+
function foldPrint(print) {
|
|
2029
|
+
return print.normalize("NFKD").replace(/\p{M}/gu, "").toLocaleLowerCase().replace(/['`\u2019]/g, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
2030
|
+
}
|
|
2031
|
+
function isColumnCutOf(a, b) {
|
|
2032
|
+
if (!comparable(a) || !comparable(b))
|
|
2033
|
+
return false;
|
|
2034
|
+
const cutFrom = (cut, whole) => {
|
|
2035
|
+
const c = foldPrint(cut.sn0);
|
|
2036
|
+
const w = foldPrint(whole.sn0);
|
|
2037
|
+
if (c.length === 0 || c.length >= w.length || !w.startsWith(c))
|
|
2038
|
+
return false;
|
|
2039
|
+
if (w[c.length] === " ")
|
|
2040
|
+
return false;
|
|
2041
|
+
const words = realWords(cut);
|
|
2042
|
+
const held = new Set(realWords(whole));
|
|
2043
|
+
return words.length > 0 && words.every((t) => held.has(t));
|
|
2044
|
+
};
|
|
2045
|
+
return cutFrom(a, b) || cutFrom(b, a);
|
|
2046
|
+
}
|
|
2047
|
+
function isInitialismOf(a, b, which) {
|
|
2048
|
+
if (!comparable(a) || !comparable(b))
|
|
2049
|
+
return false;
|
|
2050
|
+
const spells = (bare, full) => {
|
|
2051
|
+
const words = realWords(bare);
|
|
2052
|
+
if (words.length !== 1 || realWords(full).length === 0)
|
|
2053
|
+
return false;
|
|
2054
|
+
const token = words[0];
|
|
2055
|
+
if (probablySameSchool(bare, full, "realWords"))
|
|
2056
|
+
return false;
|
|
2057
|
+
const { declared, derived } = schoolInitials(full);
|
|
2058
|
+
if (declared === token)
|
|
2059
|
+
return true;
|
|
2060
|
+
return which === "derived" && derived.includes(token);
|
|
2061
|
+
};
|
|
2062
|
+
return spells(a, b) || spells(b, a);
|
|
2063
|
+
}
|
|
2064
|
+
function sameInEveryPart(a, b) {
|
|
2065
|
+
if (!probablySameSchool(a, b, "realWords"))
|
|
2066
|
+
return false;
|
|
2067
|
+
if (a.institution !== b.institution)
|
|
2068
|
+
return false;
|
|
2069
|
+
if (a.location === null || b.location === null)
|
|
2070
|
+
return a.location === b.location;
|
|
2071
|
+
if (placeRelation2(a.location, b.location) === "same")
|
|
2072
|
+
return true;
|
|
2073
|
+
return sameReading2(a.location, b.location);
|
|
2074
|
+
}
|
|
2075
|
+
function schoolCompare(a, b, dop) {
|
|
2076
|
+
requireLexicon(dop, a, b);
|
|
2077
|
+
const x = keyAt(a, dop);
|
|
2078
|
+
const y = keyAt(b, dop);
|
|
2079
|
+
if (x < y)
|
|
2080
|
+
return -1;
|
|
2081
|
+
return x > y ? 1 : 0;
|
|
2082
|
+
}
|
|
2083
|
+
var loner = 0;
|
|
2084
|
+
function slotOf(name, dop) {
|
|
2085
|
+
requireLexicon(dop, name);
|
|
2086
|
+
const tokens = keyTokens(name, dop);
|
|
2087
|
+
return tokens === null ? null : tokens.join(UNIT);
|
|
2088
|
+
}
|
|
2089
|
+
function setOfSchools(dop) {
|
|
2090
|
+
const held = new Map;
|
|
2091
|
+
const set = {
|
|
2092
|
+
dop,
|
|
2093
|
+
get size() {
|
|
2094
|
+
return held.size;
|
|
2095
|
+
},
|
|
2096
|
+
get: (name) => {
|
|
2097
|
+
const slot = slotOf(name, dop);
|
|
2098
|
+
return slot === null ? undefined : held.get(slot);
|
|
2099
|
+
},
|
|
2100
|
+
has: (name) => {
|
|
2101
|
+
const slot = slotOf(name, dop);
|
|
2102
|
+
return slot === null ? false : held.has(slot);
|
|
2103
|
+
},
|
|
2104
|
+
add(name) {
|
|
2105
|
+
const slot = slotOf(name, dop);
|
|
2106
|
+
if (slot === null)
|
|
2107
|
+
held.set(`${UNIT}${UNIT}${loner++}`, name);
|
|
2108
|
+
else if (!held.has(slot))
|
|
2109
|
+
held.set(slot, name);
|
|
2110
|
+
return this;
|
|
2111
|
+
},
|
|
2112
|
+
delete: (name) => {
|
|
2113
|
+
const slot = slotOf(name, dop);
|
|
2114
|
+
return slot === null ? false : held.delete(slot);
|
|
2115
|
+
},
|
|
2116
|
+
clear: () => held.clear(),
|
|
2117
|
+
values: () => held.values(),
|
|
2118
|
+
[Symbol.iterator]: () => held.values()
|
|
2119
|
+
};
|
|
2120
|
+
return set;
|
|
2121
|
+
}
|
|
2122
|
+
function mapOfSchools(dop, entries) {
|
|
2123
|
+
const held = new Map;
|
|
2124
|
+
const map = {
|
|
2125
|
+
dop,
|
|
2126
|
+
get size() {
|
|
2127
|
+
return held.size;
|
|
2128
|
+
},
|
|
2129
|
+
get: (name) => {
|
|
2130
|
+
const slot = slotOf(name, dop);
|
|
2131
|
+
return slot === null ? undefined : held.get(slot)?.[1];
|
|
2132
|
+
},
|
|
2133
|
+
has: (name) => {
|
|
2134
|
+
const slot = slotOf(name, dop);
|
|
2135
|
+
return slot === null ? false : held.has(slot);
|
|
2136
|
+
},
|
|
2137
|
+
set(name, value) {
|
|
2138
|
+
const slot = slotOf(name, dop);
|
|
2139
|
+
if (slot === null) {
|
|
2140
|
+
held.set(`${UNIT}${UNIT}${loner++}`, [name, value]);
|
|
2141
|
+
return this;
|
|
2142
|
+
}
|
|
2143
|
+
const found = held.get(slot);
|
|
2144
|
+
if (found === undefined)
|
|
2145
|
+
held.set(slot, [name, value]);
|
|
2146
|
+
else
|
|
2147
|
+
found[1] = value;
|
|
2148
|
+
return this;
|
|
2149
|
+
},
|
|
2150
|
+
delete: (name) => {
|
|
2151
|
+
const slot = slotOf(name, dop);
|
|
2152
|
+
return slot === null ? false : held.delete(slot);
|
|
2153
|
+
},
|
|
2154
|
+
clear: () => held.clear(),
|
|
2155
|
+
*keys() {
|
|
2156
|
+
for (const [name] of held.values())
|
|
2157
|
+
yield name;
|
|
2158
|
+
},
|
|
2159
|
+
*values() {
|
|
2160
|
+
for (const [, value] of held.values())
|
|
2161
|
+
yield value;
|
|
2162
|
+
},
|
|
2163
|
+
*entries() {
|
|
2164
|
+
for (const slot of held.values())
|
|
2165
|
+
yield [slot[0], slot[1]];
|
|
2166
|
+
},
|
|
2167
|
+
[Symbol.iterator]() {
|
|
2168
|
+
return this.entries();
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
for (const [name, value] of entries ?? [])
|
|
2172
|
+
map.set(name, value);
|
|
2173
|
+
return map;
|
|
2174
|
+
}
|
|
2175
|
+
var SCHOOL_LEVEL_COLUMNS = ["sn0", "sn1", "sn2", "sn3", "sn4", "realWords"];
|
|
2176
|
+
function schoolColumns(name) {
|
|
2177
|
+
return {
|
|
2178
|
+
sn0: slotOf(name, "sn0"),
|
|
2179
|
+
sn1: slotOf(name, "sn1"),
|
|
2180
|
+
sn2: slotOf(name, "sn2"),
|
|
2181
|
+
sn3: slotOf(name, "sn3"),
|
|
2182
|
+
sn4: slotOf(name, "sn4"),
|
|
2183
|
+
realWords: slotOf(name, "realWords")
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function isLadderLevel(dop) {
|
|
2187
|
+
return LADDER.has(dop);
|
|
2188
|
+
}
|
|
2189
|
+
function schoolEncoder(options = {}) {
|
|
2190
|
+
const reader = options.reader ?? DEFAULT_SCHOOL_NAME_READER;
|
|
2191
|
+
const printCol = options.print ?? "print";
|
|
2192
|
+
return {
|
|
2193
|
+
columns: [printCol, ...SCHOOL_LEVEL_COLUMNS, "matchKey"],
|
|
2194
|
+
levels: SCHOOL_LEVEL_COLUMNS,
|
|
2195
|
+
indexed: [...SCHOOL_LEVEL_COLUMNS, "matchKey"],
|
|
2196
|
+
encode(x) {
|
|
2197
|
+
const key = foldSchoolName(x.sn0);
|
|
2198
|
+
return { [printCol]: x.sn0, ...schoolColumns(x), matchKey: key === "" ? null : key };
|
|
2199
|
+
},
|
|
2200
|
+
key: (x, level) => schoolColumns(x)[level],
|
|
2201
|
+
decode: (row) => reader.schoolName(row[printCol] ?? "")
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
function compareSchoolNames(a, b, reader) {
|
|
2205
|
+
return compareSchools(reader.schoolName(a), reader.schoolName(b));
|
|
2206
|
+
}
|
|
2207
|
+
function compareSchools(a, b) {
|
|
2208
|
+
if (!a.isSchool() || !b.isSchool())
|
|
2209
|
+
return { verdict: "AMBIGUOUS", reason: "no-school", sharesNameParts: false };
|
|
2210
|
+
if (realWords(a).length === 0 || realWords(b).length === 0)
|
|
2211
|
+
return { verdict: "AMBIGUOUS", reason: "no-name-part", sharesNameParts: false };
|
|
2212
|
+
const cut = isColumnCutOf(a, b);
|
|
2213
|
+
if (!cut && !probablySameSchool(a, b, "realWords")) {
|
|
2214
|
+
if (differOnlyByDamagedGenerics(a, b))
|
|
2215
|
+
return { verdict: "AMBIGUOUS", reason: "damaged-generic-word", sharesNameParts: true };
|
|
2216
|
+
if (isInitialismOf(a, b, "derived")) {
|
|
2217
|
+
if (a.location !== null && b.location !== null) {
|
|
2218
|
+
if (placeRelation2(a.location, b.location) === "conflict")
|
|
2219
|
+
return { verdict: "BLOCKED", reason: "location-conflict", sharesNameParts: true };
|
|
2220
|
+
}
|
|
2221
|
+
return isInitialismOf(a, b, "declared") ? { verdict: "CANDIDATE", reason: "declared-initialism", sharesNameParts: true } : { verdict: "AMBIGUOUS", reason: "derived-initialism", sharesNameParts: true };
|
|
2222
|
+
}
|
|
2223
|
+
return { verdict: "DIFFERENT", reason: "name-parts-differ", sharesNameParts: false };
|
|
2224
|
+
}
|
|
2225
|
+
const sharesNameParts = true;
|
|
2226
|
+
if (a.institution !== null && b.institution !== null && a.institution !== b.institution)
|
|
2227
|
+
return { verdict: "BLOCKED", reason: "institution-type-conflict", sharesNameParts };
|
|
2228
|
+
if (a.location !== null && b.location !== null) {
|
|
2229
|
+
if (placeRelation2(a.location, b.location) === "conflict")
|
|
2230
|
+
return { verdict: "BLOCKED", reason: "location-conflict", sharesNameParts };
|
|
2231
|
+
}
|
|
2232
|
+
if (a.institution === null !== (b.institution === null))
|
|
2233
|
+
return { verdict: "AMBIGUOUS", reason: "unqualified-vs-qualified", sharesNameParts };
|
|
2234
|
+
if (a.location === null !== (b.location === null))
|
|
2235
|
+
return { verdict: "AMBIGUOUS", reason: "unqualified-vs-qualified", sharesNameParts };
|
|
2236
|
+
if (a.concerns.includes("promoted-name") || b.concerns.includes("promoted-name"))
|
|
2237
|
+
return { verdict: "AMBIGUOUS", reason: "promoted-token-identity", sharesNameParts };
|
|
2238
|
+
if (cut && !probablySameSchool(a, b, "realWords"))
|
|
2239
|
+
return { verdict: "CANDIDATE", reason: "truncated-print", sharesNameParts };
|
|
2240
|
+
return { verdict: "CANDIDATE", reason: "same-name-and-type", sharesNameParts };
|
|
2241
|
+
}
|
|
2242
|
+
export {
|
|
2243
|
+
DEFAULT_SCHOOL_NAME_CAPACITY,
|
|
2244
|
+
DEFAULT_SCHOOL_NAME_READER,
|
|
2245
|
+
NOT_A_SCHOOL,
|
|
2246
|
+
SCHOOL_LEVEL_COLUMNS,
|
|
2247
|
+
SCHOOL_NAME_EXCEPTIONS,
|
|
2248
|
+
compareSchoolNames,
|
|
2249
|
+
compareSchools,
|
|
2250
|
+
createSchoolNameReader,
|
|
2251
|
+
foldSchoolName,
|
|
2252
|
+
hasSpecificToken,
|
|
2253
|
+
isColumnCutOf,
|
|
2254
|
+
isInitialismOf,
|
|
2255
|
+
isLadderLevel,
|
|
2256
|
+
mapOfSchools,
|
|
2257
|
+
probablySameSchool,
|
|
2258
|
+
realWords,
|
|
2259
|
+
realWordsInPrintOrder,
|
|
2260
|
+
sameInEveryPart,
|
|
2261
|
+
schoolColumns,
|
|
2262
|
+
schoolCompare,
|
|
2263
|
+
schoolEncoder,
|
|
2264
|
+
schoolInitials,
|
|
2265
|
+
schoolKeyTokens,
|
|
2266
|
+
schoolPrintSplit,
|
|
2267
|
+
schoolTokensAt,
|
|
2268
|
+
setOfSchools
|
|
2269
|
+
};
|