@mailwoman/kind-classifier 8.1.0 → 8.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/classify.ts +5 -2
- package/index.ts +2 -0
- package/out/classify.d.ts +3 -1
- package/out/classify.d.ts.map +1 -1
- package/out/classify.js +1 -1
- package/out/classify.js.map +1 -1
- package/out/index.d.ts.map +1 -1
- package/out/index.js.map +1 -1
- package/out/poi.d.ts +18 -6
- package/out/poi.d.ts.map +1 -1
- package/out/poi.js +11 -4
- package/out/poi.js.map +1 -1
- package/out/rules.d.ts.map +1 -1
- package/out/rules.js +58 -14
- package/out/rules.js.map +1 -1
- package/out/types.d.ts +3 -1
- package/out/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/poi.ts +30 -10
- package/rules.ts +66 -14
- package/types.ts +3 -1
package/classify.ts
CHANGED
|
@@ -49,6 +49,7 @@ export function classifyKindSync(input: NormalizedInputLite, shape: QueryShapeLi
|
|
|
49
49
|
const scored = SCORERS.map((s) => ({ kind: s.kind, confidence: s.score(input, shape) })).filter(
|
|
50
50
|
(s) => s.confidence > 0
|
|
51
51
|
)
|
|
52
|
+
|
|
52
53
|
scored.sort((a, b) => b.confidence - a.confidence)
|
|
53
54
|
|
|
54
55
|
const top = scored[0] ?? { kind: "vague" as QueryKind, confidence: 0.3 }
|
|
@@ -74,7 +75,9 @@ export async function classifyKind(
|
|
|
74
75
|
return classifyKindSync(input, shape)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Options for {@link createKindClassifier}.
|
|
80
|
+
*/
|
|
78
81
|
export interface KindClassifierOpts {
|
|
79
82
|
/**
|
|
80
83
|
* POI phrase lexicon (spec §3.1). When present, a `poi_query` scorer joins the rule set — injected, never imported,
|
|
@@ -114,7 +117,7 @@ export function createKindClassifier(
|
|
|
114
117
|
|
|
115
118
|
return {
|
|
116
119
|
...base,
|
|
117
|
-
alternatives: [...base.alternatives, poiAlternative].
|
|
120
|
+
alternatives: [...base.alternatives, poiAlternative].toSorted((a, b) => b.confidence - a.confidence),
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
}
|
package/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { classifyKind, classifyKindSync, createKindClassifier } from "./classify
|
|
|
17
17
|
export type { KindClassifierOpts } from "./classify.ts"
|
|
18
18
|
export { matchPOISubject } from "./poi.ts"
|
|
19
19
|
export type { POIPhraseMatch, POIPhraseLookup, POISubjectMatch } from "./poi.ts"
|
|
20
|
+
|
|
20
21
|
export {
|
|
21
22
|
scoreIntersection,
|
|
22
23
|
scoreLandmark,
|
|
@@ -26,4 +27,5 @@ export {
|
|
|
26
27
|
scoreStructuredAddress,
|
|
27
28
|
scoreVague,
|
|
28
29
|
} from "./rules.ts"
|
|
30
|
+
|
|
29
31
|
export type { LocaleHint, NormalizedInputLite, QueryKind, QueryKindResult, QueryShapeLike } from "./types.ts"
|
package/out/classify.d.ts
CHANGED
|
@@ -25,7 +25,9 @@ export declare function classifyKindSync(input: NormalizedInputLite, shape: Quer
|
|
|
25
25
|
* The locale parameter is accepted for future locale-aware rules (Japanese honorifics, etc.) but not currently used.
|
|
26
26
|
*/
|
|
27
27
|
export declare function classifyKind(input: NormalizedInputLite, shape: QueryShapeLike, _locale?: LocaleHint): Promise<QueryKindResult>;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Options for {@link createKindClassifier}.
|
|
30
|
+
*/
|
|
29
31
|
export interface KindClassifierOpts {
|
|
30
32
|
/**
|
|
31
33
|
* POI phrase lexicon (spec §3.1). When present, a `poi_query` scorer joins the rule set — injected, never imported,
|
package/out/classify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAWpE,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAa,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAiB7G;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAWpE,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAa,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAiB7G;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,eAAe,CAenG;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CACjC,KAAK,EAAE,mBAAmB,EAC1B,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,IAAI,GAAE,kBAAuB,GAC3B,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,UAAU,KAAK,OAAO,CAAC,eAAe,CAAC,CA2BtG"}
|
package/out/classify.js
CHANGED
|
@@ -70,7 +70,7 @@ export function createKindClassifier(opts = {}) {
|
|
|
70
70
|
const poiAlternative = { kind: "poi_query", confidence: poiConfidence };
|
|
71
71
|
return {
|
|
72
72
|
...base,
|
|
73
|
-
alternatives: [...base.alternatives, poiAlternative].
|
|
73
|
+
alternatives: [...base.alternatives, poiAlternative].toSorted((a, b) => b.confidence - a.confidence),
|
|
74
74
|
};
|
|
75
75
|
};
|
|
76
76
|
}
|
package/out/classify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.js","sourceRoot":"","sources":["../classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,mBAAmB,EAAwB,MAAM,UAAU,CAAA;AACpE,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,EACV,kBAAkB,GAClB,MAAM,YAAY,CAAA;AAQnB,MAAM,OAAO,GAA8B;IAC1C,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE;IACrC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;IAC9F,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,iBAAiB,EAAE;IAClD,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACnD,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACnD,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAC7D,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE;CACpC,CAAA;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA0B,EAAE,KAAqB;IACjF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAC9F,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CACvB,CAAA;
|
|
1
|
+
{"version":3,"file":"classify.js","sourceRoot":"","sources":["../classify.ts"],"names":[],"mappings":"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"}
|
package/out/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAEhF,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,GACV,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,mBAAmB,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
|
package/out/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEpF,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEpF,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG1C,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,GACV,MAAM,YAAY,CAAA"}
|
package/out/poi.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* `poiQueryKind` flag (default-ON since 2026-07-20). Spec §3.1.
|
|
10
10
|
*/
|
|
11
11
|
import type { NormalizedInputLite, QueryShapeLike } from "./types.ts";
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* One lexicon hit for a candidate subject phrase.
|
|
14
|
+
*/
|
|
13
15
|
export interface POIPhraseMatch {
|
|
14
16
|
/**
|
|
15
17
|
* The matched subject's identifier string. For `kind: "category"`, a `@mailwoman/poi-taxonomy` category id. For
|
|
@@ -19,22 +21,32 @@ export interface POIPhraseMatch {
|
|
|
19
21
|
categoryID: string;
|
|
20
22
|
matchedPhrase: string;
|
|
21
23
|
confidence: number;
|
|
22
|
-
/** Which lexicon this hit came from. Existing category lookups set `"category"` (backward-compatible default). */
|
|
23
24
|
/**
|
|
24
25
|
* Absent = "category" (the pre-brand shape) — optional so pre-7.3 POIPhraseLookup implementors stay
|
|
25
26
|
* source-compatible.
|
|
26
27
|
*/
|
|
27
28
|
kind?: "category" | "brand";
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Wikidata QID, when known. `kind: "brand"` only — absent when a brand resolved by name alone (no QID match).
|
|
31
|
+
*/
|
|
29
32
|
wikidata?: string;
|
|
30
33
|
}
|
|
31
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Injected phrase→category lookup. Exact-phrase, locale-aware; returns [] on miss.
|
|
36
|
+
*/
|
|
32
37
|
export type POIPhraseLookup = (phrase: string, locale?: string) => ReadonlyArray<POIPhraseMatch>;
|
|
38
|
+
/**
|
|
39
|
+
* Which lexicon this hit came from. Existing category lookups set `"category"` (backward-compatible default).
|
|
40
|
+
*/
|
|
33
41
|
export interface POISubjectMatch {
|
|
34
42
|
match: POIPhraseMatch;
|
|
35
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The matched subject text as it appeared in the query.
|
|
45
|
+
*/
|
|
36
46
|
subject: string;
|
|
37
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The anchor remainder after the separator; `""` when the whole input matched.
|
|
49
|
+
*/
|
|
38
50
|
remainder: string;
|
|
39
51
|
}
|
|
40
52
|
/**
|
package/out/poi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poi.d.ts","sourceRoot":"","sources":["../poi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"poi.d.ts","sourceRoot":"","sources":["../poi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAQrE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAA;IAC3B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,aAAa,CAAC,cAAc,CAAC,CAAA;AAEhG;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,cAAc,CAAA;IACrB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;CACjB;AA2BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,EAAE,eAAe,GACrB,eAAe,GAAG,IAAI,CA6BxB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,MAAM,GACb,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,KAAK,MAAM,CAiB/D"}
|
package/out/poi.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* lives in `@mailwoman/poi-taxonomy` and is wired in by `createRuntimePipeline` behind the
|
|
9
9
|
* `poiQueryKind` flag (default-ON since 2026-07-20). Spec §3.1.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* Comma-segment ceiling for a POI-led query. Past it the input is a venue plus a full address (`X, 350 5th Ave, New
|
|
13
|
+
* York, NY`), which the structured-address scorer should claim instead.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_POI_SEGMENTS = 3;
|
|
11
16
|
/**
|
|
12
17
|
* Anchor separator between subject and place: comma, or near/in/at/around — scanned left-to-right until a prefix hits
|
|
13
18
|
* the lexicon.
|
|
@@ -27,7 +32,9 @@
|
|
|
27
32
|
* (systematic + fuzzed adversarial whitespace + shared-whitespace anchor/comma chains).
|
|
28
33
|
*/
|
|
29
34
|
const ANCHOR_SEPARATOR = /,\s*|\s(?:near|in|at|around)\s+/gi;
|
|
30
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Longest subject we accept, in tokens. Lexicon phrases are short; 4 covers the table.
|
|
37
|
+
*/
|
|
31
38
|
const MAX_SUBJECT_TOKENS = 4;
|
|
32
39
|
/**
|
|
33
40
|
* Match a POI subject: the whole input, or the text before the FIRST anchor separator WHOSE PREFIX HITS THE LEXICON (≤
|
|
@@ -40,7 +47,7 @@ export function matchPOISubject(text, locale, lookup) {
|
|
|
40
47
|
if (!trimmed)
|
|
41
48
|
return null;
|
|
42
49
|
const whole = lookup(trimmed, locale);
|
|
43
|
-
if (whole.length
|
|
50
|
+
if (whole.length) {
|
|
44
51
|
return { match: whole[0], subject: trimmed, remainder: "" };
|
|
45
52
|
}
|
|
46
53
|
for (const separator of trimmed.matchAll(ANCHOR_SEPARATOR)) {
|
|
@@ -51,7 +58,7 @@ export function matchPOISubject(text, locale, lookup) {
|
|
|
51
58
|
if (subject.split(/\s+/).length > MAX_SUBJECT_TOKENS)
|
|
52
59
|
break;
|
|
53
60
|
const hits = lookup(subject, locale);
|
|
54
|
-
if (hits.length
|
|
61
|
+
if (!hits.length)
|
|
55
62
|
continue;
|
|
56
63
|
const remainder = trimmed.slice(separator.index + separator[0].length).trim();
|
|
57
64
|
return { match: hits[0], subject, remainder };
|
|
@@ -75,7 +82,7 @@ export function createScorePOIQuery(lookup, locale) {
|
|
|
75
82
|
if (/^\d+\s/.test(matched.remainder))
|
|
76
83
|
return 0;
|
|
77
84
|
const segCount = shape.segments?.length ?? 1;
|
|
78
|
-
if (segCount >
|
|
85
|
+
if (segCount > MAX_POI_SEGMENTS)
|
|
79
86
|
return 0;
|
|
80
87
|
return 0.9 * matched.match.confidence;
|
|
81
88
|
};
|
package/out/poi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poi.js","sourceRoot":"","sources":["../poi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"poi.js","sourceRoot":"","sources":["../poi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;GAGG;AACH,MAAM,gBAAgB,GAAG,CAAC,CAAA;AA6C1B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,gBAAgB,GAAG,mCAAmC,CAAA;AAE5D;;GAEG;AACH,MAAM,kBAAkB,GAAG,CAAC,CAAA;AAE5B;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC9B,IAAY,EACZ,MAA0B,EAC1B,MAAuB;IAEvB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IAE3B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IAEzB,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAErC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAE,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAA;IAC7D,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC5D,IAAI,SAAS,CAAC,KAAK,KAAK,CAAC;YAAE,SAAQ;QAEnC,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAA;QAExD,uFAAuF;QACvF,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,kBAAkB;YAAE,MAAK;QAE3D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAEpC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,SAAQ;QAE1B,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;QAE7E,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAE,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IAC/C,CAAC;IAED,OAAO,IAAI,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAClC,MAAuB,EACvB,MAAe;IAEf,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,IAAI,KAAK,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;QAExF,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,CAAA;QAEtB,IAAI,OAAO,CAAC,SAAS,KAAK,EAAE;YAAE,OAAO,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAA;QAEpE,gFAAgF;QAChF,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;YAAE,OAAO,CAAC,CAAA;QAE9C,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,CAAA;QAE5C,IAAI,QAAQ,GAAG,gBAAgB;YAAE,OAAO,CAAC,CAAA;QAEzC,OAAO,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAA;IACtC,CAAC,CAAA;AACF,CAAC"}
|
package/out/rules.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAwErE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAQrF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAQ5F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAQxF;AA+BD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAkD5F;AAgBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAc3F;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAe3F;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAmBhG;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAEtF"}
|
package/out/rules.js
CHANGED
|
@@ -9,7 +9,45 @@
|
|
|
9
9
|
* Bitter-lesson-safe: only universal structural patterns — no place-name dictionaries. ~1 small
|
|
10
10
|
* regex set per new locale, not 50K dictionary entries.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Longest input still plausible as a bare venue or landmark name. Beyond it the query is carrying an address as well,
|
|
14
|
+
* and belongs to the structured-address scorer.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_LANDMARK_LENGTH = 50;
|
|
17
|
+
/**
|
|
18
|
+
* Longest input still plausible as a bare postcode, allowing for a country prefix and separators.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_POSTCODE_ONLY_LENGTH = 16;
|
|
21
|
+
/**
|
|
22
|
+
* Share of the input the postcode must occupy before `postcode_only` fires. Below it the query is carrying something
|
|
23
|
+
* else too — a locality, a street — and another kind should win.
|
|
24
|
+
*/
|
|
25
|
+
const MIN_POSTCODE_COVERAGE = 0.7;
|
|
26
|
+
/**
|
|
27
|
+
* Longest input still plausible as a bare locality name, including a trailing region code.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_LOCALITY_ONLY_LENGTH = 30;
|
|
30
|
+
/**
|
|
31
|
+
* Word count of a short capitalized phrase — the shape of a venue name like `Empire State Building`. Wider than this
|
|
32
|
+
* and the phrase is more likely a full address line.
|
|
33
|
+
*/
|
|
34
|
+
const VENUE_PHRASE_MIN_WORDS = 2;
|
|
35
|
+
/**
|
|
36
|
+
* Upper bound of the short-phrase venue window; see {@link VENUE_PHRASE_MIN_WORDS}.
|
|
37
|
+
*/
|
|
38
|
+
const VENUE_PHRASE_MAX_WORDS = 4;
|
|
39
|
+
/**
|
|
40
|
+
* Word count above which a single-segment proper-case phrase stops reading as a venue name.
|
|
41
|
+
*/
|
|
42
|
+
const LONG_VENUE_PHRASE_MAX_WORDS = 6;
|
|
43
|
+
/**
|
|
44
|
+
* Length at which a single-segment alphanumeric input reads as a full postcode rather than a fragment. Shorter
|
|
45
|
+
* alphanumeric inputs score lower because they are as likely to be a unit number.
|
|
46
|
+
*/
|
|
47
|
+
const ALPHANUMERIC_POSTCODE_MIN_LENGTH = 15;
|
|
48
|
+
/**
|
|
49
|
+
* Landmark vocabulary — phrases that suggest a vague-location description rather than an address.
|
|
50
|
+
*/
|
|
13
51
|
const LANDMARK_LEADERS = [
|
|
14
52
|
"behind",
|
|
15
53
|
"near",
|
|
@@ -21,7 +59,9 @@ const LANDMARK_LEADERS = [
|
|
|
21
59
|
"close to",
|
|
22
60
|
"beside",
|
|
23
61
|
];
|
|
24
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Intersection vocabulary — words that signal "where two streets cross" rather than an address.
|
|
64
|
+
*/
|
|
25
65
|
const INTERSECTION_PATTERNS = [
|
|
26
66
|
/\bcorner of\b/i,
|
|
27
67
|
/\bintersection of\b/i,
|
|
@@ -64,7 +104,9 @@ export function scoreLandmark(input, _shape) {
|
|
|
64
104
|
}
|
|
65
105
|
return 0;
|
|
66
106
|
}
|
|
67
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Street-suffix tokens that indicate an address, not a venue name.
|
|
109
|
+
*/
|
|
68
110
|
const STREET_SUFFIXES = new Set([
|
|
69
111
|
"st",
|
|
70
112
|
"street",
|
|
@@ -100,13 +142,13 @@ const STREET_SUFFIXES = new Set([
|
|
|
100
142
|
export function scoreVenueLandmark(input, shape) {
|
|
101
143
|
const text = input.normalized.trim();
|
|
102
144
|
const len = text.length;
|
|
103
|
-
if (len === 0 || len >
|
|
145
|
+
if (len === 0 || len > MAX_LANDMARK_LENGTH)
|
|
104
146
|
return 0;
|
|
105
147
|
// Must have at least one capitalized word.
|
|
106
148
|
if (!/[A-Z]/.test(text))
|
|
107
149
|
return 0;
|
|
108
150
|
// Reject if any known postcode format hit exists.
|
|
109
|
-
if (shape.knownFormats.length
|
|
151
|
+
if (shape.knownFormats.length)
|
|
110
152
|
return 0;
|
|
111
153
|
// Reject if it looks like a multi-segment structured address (City, ST ZIP).
|
|
112
154
|
const segCount = shape.segments?.length ?? 1;
|
|
@@ -127,7 +169,7 @@ export function scoreVenueLandmark(input, shape) {
|
|
|
127
169
|
const allProperCase = words.length > 1 && words.every((w) => /^[A-Z]/.test(w));
|
|
128
170
|
// Boost for short single-segment capitalized phrases (2-4 words).
|
|
129
171
|
const wordCount = words.length;
|
|
130
|
-
if (wordCount >=
|
|
172
|
+
if (wordCount >= VENUE_PHRASE_MIN_WORDS && wordCount <= VENUE_PHRASE_MAX_WORDS && segCount === 1) {
|
|
131
173
|
if (hasInternalNumber)
|
|
132
174
|
return 0.88;
|
|
133
175
|
if (allProperCase)
|
|
@@ -135,12 +177,14 @@ export function scoreVenueLandmark(input, shape) {
|
|
|
135
177
|
return 0.65;
|
|
136
178
|
}
|
|
137
179
|
// Longer single-segment capitalized phrases get moderate confidence.
|
|
138
|
-
if (wordCount <=
|
|
180
|
+
if (wordCount <= LONG_VENUE_PHRASE_MAX_WORDS && segCount === 1 && allProperCase) {
|
|
139
181
|
return 0.75;
|
|
140
182
|
}
|
|
141
183
|
return 0;
|
|
142
184
|
}
|
|
143
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Known QueryShape format strings that indicate "this token is a postcode".
|
|
187
|
+
*/
|
|
144
188
|
const POSTCODE_FORMATS = new Set([
|
|
145
189
|
"us_zip",
|
|
146
190
|
"us_zip4",
|
|
@@ -166,14 +210,14 @@ export function isPostcodeFormat(format) {
|
|
|
166
210
|
*/
|
|
167
211
|
export function scorePostcodeOnly(input, shape) {
|
|
168
212
|
const len = input.normalized.length;
|
|
169
|
-
if (len === 0 || len >
|
|
213
|
+
if (len === 0 || len > MAX_POSTCODE_ONLY_LENGTH)
|
|
170
214
|
return 0;
|
|
171
215
|
const postcodeHit = shape.knownFormats.find((f) => isPostcodeFormat(f.format));
|
|
172
216
|
if (!postcodeHit)
|
|
173
217
|
return 0;
|
|
174
218
|
const hitLen = postcodeHit.span.end - postcodeHit.span.start;
|
|
175
219
|
// At least 70% of the input must be the postcode for the rule to fire confidently.
|
|
176
|
-
if (hitLen / len <
|
|
220
|
+
if (hitLen / len < MIN_POSTCODE_COVERAGE)
|
|
177
221
|
return 0;
|
|
178
222
|
// Confidence scales with how much of the input is the postcode and how confident the format hit was.
|
|
179
223
|
return Math.min(1, postcodeHit.confidence * (hitLen / len) + 0.1);
|
|
@@ -186,11 +230,11 @@ export function scorePostcodeOnly(input, shape) {
|
|
|
186
230
|
*/
|
|
187
231
|
export function scoreLocalityOnly(input, shape) {
|
|
188
232
|
const len = input.normalized.length;
|
|
189
|
-
if (len === 0 || len >
|
|
233
|
+
if (len === 0 || len > MAX_LOCALITY_ONLY_LENGTH)
|
|
190
234
|
return 0;
|
|
191
235
|
if (shape.characterClass !== "alpha")
|
|
192
236
|
return 0;
|
|
193
|
-
if (shape.knownFormats.length
|
|
237
|
+
if (shape.knownFormats.length)
|
|
194
238
|
return 0;
|
|
195
239
|
// Locality-only inputs typically have 1-3 segments (e.g. "New York" is 1 segment, "Paris, FR" is 2).
|
|
196
240
|
// We allow up to 2 segments before deciding it's structured.
|
|
@@ -212,13 +256,13 @@ export function scoreStructuredAddress(input, shape) {
|
|
|
212
256
|
if (segCount >= 2 && shape.characterClass === "alphanumeric")
|
|
213
257
|
return 0.9;
|
|
214
258
|
// Single-segment but reasonably long and alphanumeric = moderate confidence.
|
|
215
|
-
if (len >=
|
|
259
|
+
if (len >= ALPHANUMERIC_POSTCODE_MIN_LENGTH && shape.characterClass === "alphanumeric")
|
|
216
260
|
return 0.75;
|
|
217
261
|
// Multi-segment but pure-alpha = moderate (could be a multi-word locality).
|
|
218
262
|
if (segCount >= 2)
|
|
219
263
|
return 0.6;
|
|
220
264
|
// Single-segment, short, alphanumeric (e.g. "10118-1234" with no other content) — weak.
|
|
221
|
-
if (len <
|
|
265
|
+
if (len < ALPHANUMERIC_POSTCODE_MIN_LENGTH && shape.characterClass === "alphanumeric")
|
|
222
266
|
return 0.4;
|
|
223
267
|
return 0;
|
|
224
268
|
}
|
package/out/rules.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../rules.ts"],"names":[],"mappings":"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"}
|
package/out/types.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Re-exports of the canonical types from `@mailwoman/core/pipeline`.
|
|
8
|
+
*/
|
|
7
9
|
export type { LocaleHint, QueryKind, QueryKindResult } from "@mailwoman/core/pipeline";
|
|
8
10
|
/**
|
|
9
11
|
* Minimal `NormalizedInput` shape consumed by `classifyKind`. Compatible with `@mailwoman/normalize`'s output.
|
package/out/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAEtF;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,YAAY,EAAE,aAAa,CAAC;QAC3B,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACpC,UAAU,EAAE,MAAM,CAAA;KAClB,CAAC,CAAA;IACF,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACzD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailwoman/kind-classifier",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.0",
|
|
4
4
|
"description": "Stage 2.5 of the runtime pipeline — categorize inputs by query shape (postcode_only / locality_only / structured_address / intersection / po_box / landmark / vague). Rule-based for v1.",
|
|
5
5
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
6
6
|
"repository": {
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@mailwoman/core": "8.
|
|
44
|
+
"@mailwoman/core": "8.2.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/poi.ts
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
import type { NormalizedInputLite, QueryShapeLike } from "./types.ts"
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Comma-segment ceiling for a POI-led query. Past it the input is a venue plus a full address (`X, 350 5th Ave, New
|
|
16
|
+
* York, NY`), which the structured-address scorer should claim instead.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_POI_SEGMENTS = 3
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One lexicon hit for a candidate subject phrase.
|
|
22
|
+
*/
|
|
15
23
|
export interface POIPhraseMatch {
|
|
16
24
|
/**
|
|
17
25
|
* The matched subject's identifier string. For `kind: "category"`, a `@mailwoman/poi-taxonomy` category id. For
|
|
@@ -21,24 +29,34 @@ export interface POIPhraseMatch {
|
|
|
21
29
|
categoryID: string
|
|
22
30
|
matchedPhrase: string
|
|
23
31
|
confidence: number
|
|
24
|
-
/** Which lexicon this hit came from. Existing category lookups set `"category"` (backward-compatible default). */
|
|
25
32
|
/**
|
|
26
33
|
* Absent = "category" (the pre-brand shape) — optional so pre-7.3 POIPhraseLookup implementors stay
|
|
27
34
|
* source-compatible.
|
|
28
35
|
*/
|
|
29
36
|
kind?: "category" | "brand"
|
|
30
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Wikidata QID, when known. `kind: "brand"` only — absent when a brand resolved by name alone (no QID match).
|
|
39
|
+
*/
|
|
31
40
|
wikidata?: string
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Injected phrase→category lookup. Exact-phrase, locale-aware; returns [] on miss.
|
|
45
|
+
*/
|
|
35
46
|
export type POIPhraseLookup = (phrase: string, locale?: string) => ReadonlyArray<POIPhraseMatch>
|
|
36
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Which lexicon this hit came from. Existing category lookups set `"category"` (backward-compatible default).
|
|
50
|
+
*/
|
|
37
51
|
export interface POISubjectMatch {
|
|
38
52
|
match: POIPhraseMatch
|
|
39
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The matched subject text as it appeared in the query.
|
|
55
|
+
*/
|
|
40
56
|
subject: string
|
|
41
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The anchor remainder after the separator; `""` when the whole input matched.
|
|
59
|
+
*/
|
|
42
60
|
remainder: string
|
|
43
61
|
}
|
|
44
62
|
|
|
@@ -62,7 +80,9 @@ export interface POISubjectMatch {
|
|
|
62
80
|
*/
|
|
63
81
|
const ANCHOR_SEPARATOR = /,\s*|\s(?:near|in|at|around)\s+/gi
|
|
64
82
|
|
|
65
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Longest subject we accept, in tokens. Lexicon phrases are short; 4 covers the table.
|
|
85
|
+
*/
|
|
66
86
|
const MAX_SUBJECT_TOKENS = 4
|
|
67
87
|
|
|
68
88
|
/**
|
|
@@ -82,7 +102,7 @@ export function matchPOISubject(
|
|
|
82
102
|
|
|
83
103
|
const whole = lookup(trimmed, locale)
|
|
84
104
|
|
|
85
|
-
if (whole.length
|
|
105
|
+
if (whole.length) {
|
|
86
106
|
return { match: whole[0]!, subject: trimmed, remainder: "" }
|
|
87
107
|
}
|
|
88
108
|
|
|
@@ -96,7 +116,7 @@ export function matchPOISubject(
|
|
|
96
116
|
|
|
97
117
|
const hits = lookup(subject, locale)
|
|
98
118
|
|
|
99
|
-
if (hits.length
|
|
119
|
+
if (!hits.length) continue
|
|
100
120
|
|
|
101
121
|
const remainder = trimmed.slice(separator.index + separator[0].length).trim()
|
|
102
122
|
|
|
@@ -128,7 +148,7 @@ export function createScorePOIQuery(
|
|
|
128
148
|
|
|
129
149
|
const segCount = shape.segments?.length ?? 1
|
|
130
150
|
|
|
131
|
-
if (segCount >
|
|
151
|
+
if (segCount > MAX_POI_SEGMENTS) return 0
|
|
132
152
|
|
|
133
153
|
return 0.9 * matched.match.confidence
|
|
134
154
|
}
|
package/rules.ts
CHANGED
|
@@ -12,7 +12,53 @@
|
|
|
12
12
|
|
|
13
13
|
import type { NormalizedInputLite, QueryShapeLike } from "./types.ts"
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Longest input still plausible as a bare venue or landmark name. Beyond it the query is carrying an address as well,
|
|
17
|
+
* and belongs to the structured-address scorer.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_LANDMARK_LENGTH = 50
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Longest input still plausible as a bare postcode, allowing for a country prefix and separators.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_POSTCODE_ONLY_LENGTH = 16
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Share of the input the postcode must occupy before `postcode_only` fires. Below it the query is carrying something
|
|
28
|
+
* else too — a locality, a street — and another kind should win.
|
|
29
|
+
*/
|
|
30
|
+
const MIN_POSTCODE_COVERAGE = 0.7
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Longest input still plausible as a bare locality name, including a trailing region code.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_LOCALITY_ONLY_LENGTH = 30
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Word count of a short capitalized phrase — the shape of a venue name like `Empire State Building`. Wider than this
|
|
39
|
+
* and the phrase is more likely a full address line.
|
|
40
|
+
*/
|
|
41
|
+
const VENUE_PHRASE_MIN_WORDS = 2
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Upper bound of the short-phrase venue window; see {@link VENUE_PHRASE_MIN_WORDS}.
|
|
45
|
+
*/
|
|
46
|
+
const VENUE_PHRASE_MAX_WORDS = 4
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Word count above which a single-segment proper-case phrase stops reading as a venue name.
|
|
50
|
+
*/
|
|
51
|
+
const LONG_VENUE_PHRASE_MAX_WORDS = 6
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Length at which a single-segment alphanumeric input reads as a full postcode rather than a fragment. Shorter
|
|
55
|
+
* alphanumeric inputs score lower because they are as likely to be a unit number.
|
|
56
|
+
*/
|
|
57
|
+
const ALPHANUMERIC_POSTCODE_MIN_LENGTH = 15
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Landmark vocabulary — phrases that suggest a vague-location description rather than an address.
|
|
61
|
+
*/
|
|
16
62
|
const LANDMARK_LEADERS = [
|
|
17
63
|
"behind",
|
|
18
64
|
"near",
|
|
@@ -25,7 +71,9 @@ const LANDMARK_LEADERS = [
|
|
|
25
71
|
"beside",
|
|
26
72
|
]
|
|
27
73
|
|
|
28
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Intersection vocabulary — words that signal "where two streets cross" rather than an address.
|
|
76
|
+
*/
|
|
29
77
|
const INTERSECTION_PATTERNS = [
|
|
30
78
|
/\bcorner of\b/i,
|
|
31
79
|
/\bintersection of\b/i,
|
|
@@ -75,7 +123,9 @@ export function scoreLandmark(input: NormalizedInputLite, _shape: QueryShapeLike
|
|
|
75
123
|
return 0
|
|
76
124
|
}
|
|
77
125
|
|
|
78
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Street-suffix tokens that indicate an address, not a venue name.
|
|
128
|
+
*/
|
|
79
129
|
const STREET_SUFFIXES = new Set([
|
|
80
130
|
"st",
|
|
81
131
|
"street",
|
|
@@ -113,13 +163,13 @@ export function scoreVenueLandmark(input: NormalizedInputLite, shape: QueryShape
|
|
|
113
163
|
const text = input.normalized.trim()
|
|
114
164
|
const len = text.length
|
|
115
165
|
|
|
116
|
-
if (len === 0 || len >
|
|
166
|
+
if (len === 0 || len > MAX_LANDMARK_LENGTH) return 0
|
|
117
167
|
|
|
118
168
|
// Must have at least one capitalized word.
|
|
119
169
|
if (!/[A-Z]/.test(text)) return 0
|
|
120
170
|
|
|
121
171
|
// Reject if any known postcode format hit exists.
|
|
122
|
-
if (shape.knownFormats.length
|
|
172
|
+
if (shape.knownFormats.length) return 0
|
|
123
173
|
|
|
124
174
|
// Reject if it looks like a multi-segment structured address (City, ST ZIP).
|
|
125
175
|
const segCount = shape.segments?.length ?? 1
|
|
@@ -145,7 +195,7 @@ export function scoreVenueLandmark(input: NormalizedInputLite, shape: QueryShape
|
|
|
145
195
|
// Boost for short single-segment capitalized phrases (2-4 words).
|
|
146
196
|
const wordCount = words.length
|
|
147
197
|
|
|
148
|
-
if (wordCount >=
|
|
198
|
+
if (wordCount >= VENUE_PHRASE_MIN_WORDS && wordCount <= VENUE_PHRASE_MAX_WORDS && segCount === 1) {
|
|
149
199
|
if (hasInternalNumber) return 0.88
|
|
150
200
|
|
|
151
201
|
if (allProperCase) return 0.88
|
|
@@ -154,14 +204,16 @@ export function scoreVenueLandmark(input: NormalizedInputLite, shape: QueryShape
|
|
|
154
204
|
}
|
|
155
205
|
|
|
156
206
|
// Longer single-segment capitalized phrases get moderate confidence.
|
|
157
|
-
if (wordCount <=
|
|
207
|
+
if (wordCount <= LONG_VENUE_PHRASE_MAX_WORDS && segCount === 1 && allProperCase) {
|
|
158
208
|
return 0.75
|
|
159
209
|
}
|
|
160
210
|
|
|
161
211
|
return 0
|
|
162
212
|
}
|
|
163
213
|
|
|
164
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Known QueryShape format strings that indicate "this token is a postcode".
|
|
216
|
+
*/
|
|
165
217
|
const POSTCODE_FORMATS: ReadonlySet<string> = new Set([
|
|
166
218
|
"us_zip",
|
|
167
219
|
"us_zip4",
|
|
@@ -190,14 +242,14 @@ export function isPostcodeFormat(format: string): boolean {
|
|
|
190
242
|
export function scorePostcodeOnly(input: NormalizedInputLite, shape: QueryShapeLike): number {
|
|
191
243
|
const len = input.normalized.length
|
|
192
244
|
|
|
193
|
-
if (len === 0 || len >
|
|
245
|
+
if (len === 0 || len > MAX_POSTCODE_ONLY_LENGTH) return 0
|
|
194
246
|
const postcodeHit = shape.knownFormats.find((f) => isPostcodeFormat(f.format))
|
|
195
247
|
|
|
196
248
|
if (!postcodeHit) return 0
|
|
197
249
|
const hitLen = postcodeHit.span.end - postcodeHit.span.start
|
|
198
250
|
|
|
199
251
|
// At least 70% of the input must be the postcode for the rule to fire confidently.
|
|
200
|
-
if (hitLen / len <
|
|
252
|
+
if (hitLen / len < MIN_POSTCODE_COVERAGE) return 0
|
|
201
253
|
|
|
202
254
|
// Confidence scales with how much of the input is the postcode and how confident the format hit was.
|
|
203
255
|
return Math.min(1, postcodeHit.confidence * (hitLen / len) + 0.1)
|
|
@@ -212,11 +264,11 @@ export function scorePostcodeOnly(input: NormalizedInputLite, shape: QueryShapeL
|
|
|
212
264
|
export function scoreLocalityOnly(input: NormalizedInputLite, shape: QueryShapeLike): number {
|
|
213
265
|
const len = input.normalized.length
|
|
214
266
|
|
|
215
|
-
if (len === 0 || len >
|
|
267
|
+
if (len === 0 || len > MAX_LOCALITY_ONLY_LENGTH) return 0
|
|
216
268
|
|
|
217
269
|
if (shape.characterClass !== "alpha") return 0
|
|
218
270
|
|
|
219
|
-
if (shape.knownFormats.length
|
|
271
|
+
if (shape.knownFormats.length) return 0
|
|
220
272
|
// Locality-only inputs typically have 1-3 segments (e.g. "New York" is 1 segment, "Paris, FR" is 2).
|
|
221
273
|
// We allow up to 2 segments before deciding it's structured.
|
|
222
274
|
const segCount = shape.segments?.length ?? 1
|
|
@@ -240,13 +292,13 @@ export function scoreStructuredAddress(input: NormalizedInputLite, shape: QueryS
|
|
|
240
292
|
if (segCount >= 2 && shape.characterClass === "alphanumeric") return 0.9
|
|
241
293
|
|
|
242
294
|
// Single-segment but reasonably long and alphanumeric = moderate confidence.
|
|
243
|
-
if (len >=
|
|
295
|
+
if (len >= ALPHANUMERIC_POSTCODE_MIN_LENGTH && shape.characterClass === "alphanumeric") return 0.75
|
|
244
296
|
|
|
245
297
|
// Multi-segment but pure-alpha = moderate (could be a multi-word locality).
|
|
246
298
|
if (segCount >= 2) return 0.6
|
|
247
299
|
|
|
248
300
|
// Single-segment, short, alphanumeric (e.g. "10118-1234" with no other content) — weak.
|
|
249
|
-
if (len <
|
|
301
|
+
if (len < ALPHANUMERIC_POSTCODE_MIN_LENGTH && shape.characterClass === "alphanumeric") return 0.4
|
|
250
302
|
|
|
251
303
|
return 0
|
|
252
304
|
}
|
package/types.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Re-exports of the canonical types from `@mailwoman/core/pipeline`.
|
|
9
|
+
*/
|
|
8
10
|
export type { LocaleHint, QueryKind, QueryKindResult } from "@mailwoman/core/pipeline"
|
|
9
11
|
|
|
10
12
|
/**
|