@mailwoman/kind-classifier 9.4.0 → 10.1.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/out/index.d.ts CHANGED
@@ -3,20 +3,13 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `@mailwoman/kind-classifier` — Stage 2.5 of the runtime pipeline.
7
- *
8
- * Categorize inputs into one of eight `QueryKind`s by composing rule-based scorers over the
9
- * QueryShape sub-system's output. Pure functions, no ML, no place-name dictionaries. Returns
10
- * possibilities (alternatives) alongside the top pick so the coordinator can fall back when the
11
- * winning kind isn't actionable.
12
- *
13
- * See `docs/engineering/reference/STAGES.md` § Stage 2.5 for the contract.
6
+ * Stage 2.5 classifier: compose rule-based scorers over QueryShape and return a top kind with alternatives.
14
7
  */
15
8
  export { classifyKind, classifyKindSync, createKindClassifier } from "#classify";
16
9
  export type { KindClassifierOpts } from "#classify";
17
- export { deriveIntentMarkers } from "#intent-markers";
18
- export type { IntentMarkerContext } from "#intent-markers";
19
- export { nearMeSubject, scoreBareToponym, scoreNearMe, scoreRoutePair } from "#intent-rules";
10
+ export { deriveIntentMarkers } from "#intent/markers";
11
+ export type { IntentMarkerContext } from "#intent/markers";
12
+ export { nearMeSubject, scoreBareToponym, scoreNearMe, scoreRoutePair } from "#intent/rules";
20
13
  export { matchPOICategory, matchPOISubject } from "#poi";
21
14
  export type { POIPhraseMatch, POIPhraseLookup, POIQuerySpan, POISpatialRelation, POISubjectMatch } from "#poi";
22
15
  export { scoreIntersection, scoreLandmark, scoreLocalityOnly, scorePoBox, scorePostcodeOnly, scoreStructuredAddress, scoreVague, scoreVenueLandmark, } from "#rules";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChF,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAE9G,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,EACV,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChF,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAE9G,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,EACV,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}
package/out/index.js CHANGED
@@ -3,18 +3,11 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `@mailwoman/kind-classifier` — Stage 2.5 of the runtime pipeline.
7
- *
8
- * Categorize inputs into one of eight `QueryKind`s by composing rule-based scorers over the
9
- * QueryShape sub-system's output. Pure functions, no ML, no place-name dictionaries. Returns
10
- * possibilities (alternatives) alongside the top pick so the coordinator can fall back when the
11
- * winning kind isn't actionable.
12
- *
13
- * See `docs/engineering/reference/STAGES.md` § Stage 2.5 for the contract.
6
+ * Stage 2.5 classifier: compose rule-based scorers over QueryShape and return a top kind with alternatives.
14
7
  */
15
8
  export { classifyKind, classifyKindSync, createKindClassifier } from "#classify";
16
- export { deriveIntentMarkers } from "#intent-markers";
17
- export { nearMeSubject, scoreBareToponym, scoreNearMe, scoreRoutePair } from "#intent-rules";
9
+ export { deriveIntentMarkers } from "#intent/markers";
10
+ export { nearMeSubject, scoreBareToponym, scoreNearMe, scoreRoutePair } from "#intent/rules";
18
11
  export { matchPOICategory, matchPOISubject } from "#poi";
19
12
  export { scoreIntersection, scoreLandmark, scoreLocalityOnly, scorePoBox, scorePostcodeOnly, scoreStructuredAddress, scoreVague, scoreVenueLandmark, } from "#rules";
20
13
  //# sourceMappingURL=index.js.map
package/out/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAEhF,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAGxD,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,EACV,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAEhF,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAGxD,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,EACV,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Marker derivation for the ROAD_TO_V9 §4 intent vocabulary. This code is pure and synchronous. It is the only place the
7
+ * classifier turns a fired rule into something a caller reads.
8
+ *
9
+ * Three of the four intent kinds can raise their marker here from the string by itself. The fourth,
10
+ * Stage 2.5 cannot raise `bare_toponym`'s `declared_ambiguity` from the string by itself.
11
+ * Its trigger is the dominance margin of the resolved candidate list. `mailwoman/query-intent.ts` raises it
12
+ * after the resolve against `DECISIVE_MARGIN_LOG10`. This module therefore never emits
13
+ * `declared_ambiguity`, since a marker asserting ambiguity from the string by itself would declare every
14
+ * bare city name ambiguous.
15
+ */
16
+ import type { QueryIntentMarker, QueryKind } from "@mailwoman/core/pipeline";
17
+ import type { NormalizedInputLite } from "@mailwoman/query-shape";
18
+ import { type POIPhraseLookup } from "#poi";
19
+ /**
20
+ * Context the marker builder needs beyond the scored kinds.
21
+ */
22
+ export interface IntentMarkerContext {
23
+ input: NormalizedInputLite;
24
+ /**
25
+ * The injected POI lexicon when one was wired.
26
+ *
27
+ * Absent means no `poi_category` marker can be built, consistent with the
28
+ * kind not firing without it either.
29
+ */
30
+ poiLexicon?: POIPhraseLookup;
31
+ locale?: string;
32
+ }
33
+ /**
34
+ * Build the advisories for one classified query, taking the full verdict — top plus alternatives —
35
+ * because two of the four intent kinds live in `alternatives` by design (see `intent-rules.ts`).
36
+ *
37
+ * @returns `[]` when no intent kind fired.
38
+ * Callers surface that empty array rather than dropping the field, because an
39
+ * empty array is the classifier stating it looked.
40
+ */
41
+ export declare function deriveIntentMarkers(kinds: ReadonlyArray<{
42
+ kind: QueryKind;
43
+ confidence: number;
44
+ }>, ctx: IntentMarkerContext): QueryIntentMarker[];
45
+ //# sourceMappingURL=markers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markers.d.ts","sourceRoot":"","sources":["../../lib/intent/markers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAGjE,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,MAAM,CAAA;AAE7D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,KAAK,EAAE,mBAAmB,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,EAC7D,GAAG,EAAE,mBAAmB,GACtB,iBAAiB,EAAE,CAoErB"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Marker derivation for the ROAD_TO_V9 §4 intent vocabulary. This code is pure and synchronous. It is the only place the
7
+ * classifier turns a fired rule into something a caller reads.
8
+ *
9
+ * Three of the four intent kinds can raise their marker here from the string by itself. The fourth,
10
+ * Stage 2.5 cannot raise `bare_toponym`'s `declared_ambiguity` from the string by itself.
11
+ * Its trigger is the dominance margin of the resolved candidate list. `mailwoman/query-intent.ts` raises it
12
+ * after the resolve against `DECISIVE_MARGIN_LOG10`. This module therefore never emits
13
+ * `declared_ambiguity`, since a marker asserting ambiguity from the string by itself would declare every
14
+ * bare city name ambiguous.
15
+ */
16
+ import { nearMeSubject } from "#intent/rules";
17
+ import { matchPOICategory } from "#poi";
18
+ /**
19
+ * Build the advisories for one classified query, taking the full verdict — top plus alternatives —
20
+ * because two of the four intent kinds live in `alternatives` by design (see `intent-rules.ts`).
21
+ *
22
+ * @returns `[]` when no intent kind fired.
23
+ * Callers surface that empty array rather than dropping the field, because an
24
+ * empty array is the classifier stating it looked.
25
+ */
26
+ export function deriveIntentMarkers(kinds, ctx) {
27
+ const fired = new Set(kinds.map((k) => k.kind));
28
+ const markers = [];
29
+ if (fired.has("route_pair")) {
30
+ // Whitespace-only split rather than `wordsOf`, because `route_pair` inputs are
31
+ // comma-free by construction and the tokens are re-joined verbatim into the message.
32
+ const tokens = ctx.input.normalized.trim().split(/\s+/);
33
+ markers.push({
34
+ kind: "route_pair",
35
+ code: "declared_fork",
36
+ mechanism: "kind:route_pair",
37
+ message: `"${tokens.join(" ")}" reads two ways and the pipeline is not choosing between them: two distinct places, or one place with its admin context.`,
38
+ evidence: {
39
+ tokens,
40
+ /**
41
+ * Both readings, listed.
42
+ *
43
+ * The order is stable: pair first, then the admin reading.
44
+ * A consumer can index it.
45
+ * This order does not rank the entries.
46
+ */
47
+ interpretations: ["two_toponyms", "locality_with_admin_context"],
48
+ },
49
+ });
50
+ }
51
+ if (fired.has("near_me")) {
52
+ const subject = nearMeSubject(ctx.input);
53
+ markers.push({
54
+ kind: "near_me",
55
+ code: "focus_point_required",
56
+ mechanism: "kind:near_me",
57
+ message: `"${subject}" was asked for relative to the asker, and no focus point was supplied.`,
58
+ evidence: {
59
+ subject,
60
+ /**
61
+ * The plug point is documented but not wired.
62
+ *
63
+ * `photon/` is the eventual consumer.
64
+ * Its `/api` already accepts `lat`/`lon` location-bias params.
65
+ */
66
+ focusParameter: "photon:lat/lon",
67
+ },
68
+ });
69
+ }
70
+ if (fired.has("poi_category") && ctx.poiLexicon) {
71
+ const match = matchPOICategory(ctx.input.normalized, ctx.locale ?? ctx.input.appliedLocale, ctx.poiLexicon);
72
+ if (match) {
73
+ markers.push({
74
+ kind: "poi_category",
75
+ code: "poi_category",
76
+ mechanism: "poi-taxonomy:synonym",
77
+ message: `"${match.matchedPhrase}" is a POI category with no place to search; resolution against poi.db is out of scope.`,
78
+ evidence: {
79
+ categoryID: match.categoryID,
80
+ matchedPhrase: match.matchedPhrase,
81
+ ...(match.wikidata ? { wikidata: match.wikidata } : {}),
82
+ },
83
+ });
84
+ }
85
+ }
86
+ return markers;
87
+ }
88
+ //# sourceMappingURL=markers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markers.js","sourceRoot":"","sources":["../../lib/intent/markers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAwB,MAAM,MAAM,CAAA;AAiB7D;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAClC,KAA6D,EAC7D,GAAwB;IAExB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAY,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAC1D,MAAM,OAAO,GAAwB,EAAE,CAAA;IAEvC,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7B,+EAA+E;QAC/E,qFAAqF;QACrF,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAEvD,OAAO,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,eAAe;YACrB,SAAS,EAAE,iBAAiB;YAC5B,OAAO,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,2HAA2H;YACxJ,QAAQ,EAAE;gBACT,MAAM;gBACN;;;;;;mBAMG;gBACH,eAAe,EAAE,CAAC,cAAc,EAAE,6BAA6B,CAAC;aAChE;SACD,CAAC,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAExC,OAAO,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,sBAAsB;YAC5B,SAAS,EAAE,cAAc;YACzB,OAAO,EAAE,IAAI,OAAO,yEAAyE;YAC7F,QAAQ,EAAE;gBACT,OAAO;gBACP;;;;;mBAKG;gBACH,cAAc,EAAE,gBAAgB;aAChC;SACD,CAAC,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,CAAA;QAE3G,IAAI,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,cAAc;gBACpB,IAAI,EAAE,cAAc;gBACpB,SAAS,EAAE,sBAAsB;gBACjC,OAAO,EAAE,IAAI,KAAK,CAAC,aAAa,yFAAyF;gBACzH,QAAQ,EAAE;oBACT,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,aAAa,EAAE,KAAK,CAAC,aAAa;oBAClC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACvD;aACD,CAAC,CAAA;QACH,CAAC;IACF,CAAC;IAED,OAAO,OAAO,CAAA;AACf,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * ROAD_TO_V9 §4 — the query-intent rules, with the same `(input, shape) => number` interface as
7
+ * `rules.ts` and the same bitter-lesson invariant: universal structural patterns and bounded linguistic
8
+ * categories only, never a place-name dictionary, with the POI synonym table injected exactly as
9
+ * `poi.ts` does it.
10
+ *
11
+ * `bare_toponym` and `route_pair` score below the structural kind that already owns their population
12
+ * (`locality_only`, 0.85), so they surface in `QueryKindResult.alternatives` and never as the top kind.
13
+ * The top kind is the only thing the coordinator routes on, so pinning it is what makes these additions
14
+ * answer-neutral on the bare-city-name register. Their intent travels on the marker.
15
+ *
16
+ * `near_me` does win its top slot (0.91), because there is no incumbent worth preserving: a query
17
+ * ending "near me" is not a locality.
18
+ */
19
+ import type { NormalizedInputLite, QueryShapeSegmentsView as QueryShapeLike } from "@mailwoman/query-shape";
20
+ /**
21
+ * `bare_toponym` rule: a single coherent place-name carrying no address grammar, feeding the
22
+ * declared-ambiguity path without asserting which place — that is the resolver's question,
23
+ * decided by the answer's dominance margin in `mailwoman/query-intent.ts`.
24
+ */
25
+ export declare function scoreBareToponym(input: NormalizedInputLite, shape: QueryShapeLike): number;
26
+ /**
27
+ * `route_pair` rule: exactly two toponym-shaped tokens with no token between them.
28
+ *
29
+ * The known confound is structural and unfixable here.
30
+ * "Paris London" and "Moscow Idaho" have the same string shape.
31
+ *
32
+ * A classifier needs gazetteer knowledge that Idaho is a region to distinguish them.
33
+ * ROAD_TO_V9 §4.3 therefore specifies classification plus a declared fork.
34
+ *
35
+ * Both readings appear in the marker, neither wins and the resolver keeps its existing answer.
36
+ *
37
+ * The structurally separable class is a two-token single name ("New York", "Fort Worth")
38
+ * with a toponymic head particle.
39
+ * That guard keeps the fork off the common case.
40
+ */
41
+ export declare function scoreRoutePair(input: NormalizedInputLite, shape: QueryShapeLike): number;
42
+ /**
43
+ * `near_me` rule: a subject plus a deictic locator, with no anchor.
44
+ *
45
+ * A non-empty subject is required, so a bare "near me" stays with the `landmark` leaders rule.
46
+ */
47
+ export declare function scoreNearMe(input: NormalizedInputLite, _shape: QueryShapeLike): number;
48
+ /**
49
+ * The subject of a `near_me` query is the requested category or thing with the locator stripped.
50
+ *
51
+ * It is empty when the rule would not have fired.
52
+ * The subject builds marker evidence and never a route.
53
+ */
54
+ export declare function nearMeSubject(input: NormalizedInputLite): string;
55
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../lib/intent/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,IAAI,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAkN3G;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAE1F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAgBxF;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAatF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,CAQhE"}
@@ -0,0 +1,281 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * ROAD_TO_V9 §4 — the query-intent rules, with the same `(input, shape) => number` interface as
7
+ * `rules.ts` and the same bitter-lesson invariant: universal structural patterns and bounded linguistic
8
+ * categories only, never a place-name dictionary, with the POI synonym table injected exactly as
9
+ * `poi.ts` does it.
10
+ *
11
+ * `bare_toponym` and `route_pair` score below the structural kind that already owns their population
12
+ * (`locality_only`, 0.85), so they surface in `QueryKindResult.alternatives` and never as the top kind.
13
+ * The top kind is the only thing the coordinator routes on, so pinning it is what makes these additions
14
+ * answer-neutral on the bare-city-name register. Their intent travels on the marker.
15
+ *
16
+ * `near_me` does win its top slot (0.91), because there is no incumbent worth preserving: a query
17
+ * ending "near me" is not a locality.
18
+ */
19
+ import { carriesLetter, isDisqualifyingStreetSuffix, MAX_LOCALITY_ONLY_LENGTH, wordsOf } from "#rules";
20
+ /**
21
+ * Both refinement kinds sit a whole confidence step below `locality_only`'s 0.85
22
+ * so no float-comparison accident can flip the top slot.
23
+ */
24
+ const BARE_TOPONYM_CONFIDENCE = 0.84;
25
+ /**
26
+ * Lower still for a second reason beyond the ranking discipline.
27
+ *
28
+ * A route pair is a hypothesis whose competing reading (locality + region) is more common in this corpus.
29
+ */
30
+ const ROUTE_PAIR_CONFIDENCE = 0.55;
31
+ /**
32
+ * Above `landmark`'s venue ceiling (0.88) and above `poi_query`'s anchored band (0.90),
33
+ * because a deictic tail is a stronger signal than either shape heuristic:
34
+ * no other rule explains why "me" ends the string.
35
+ */
36
+ const NEAR_ME_CONFIDENCE = 0.91;
37
+ /**
38
+ * Word ceiling for a single bare toponym: four covers the long tail that exists as one
39
+ * place name ("Newcastle upon Tyne", "Sault Sainte Marie", "Las Palmas de Gran Canaria"),
40
+ * and past it the input contains more than a name.
41
+ */
42
+ const MAX_BARE_TOPONYM_WORDS = 4;
43
+ /**
44
+ * Toponymic head particles — the bounded linguistic category that makes a multi-token
45
+ * string one place name, with the same boundary as `@mailwoman/phrase-grouper`'s
46
+ * `PLACE_NAME_PARTICLES` (which covers the infix glue `de`, `am`, `aan den`) and the one
47
+ * job of keeping `route_pair` off "New York", "San Francisco", "Fort Worth" and their kin.
48
+ *
49
+ * It is a closed morphological class rather than a gazetteer, so growing it
50
+ * with actual place names is the wrong move.
51
+ * It is case-folded on read, because "new york" is the same query.
52
+ */
53
+ const TOPONYM_HEAD_PARTICLES = new Set([
54
+ // English
55
+ "new",
56
+ "old",
57
+ "fort",
58
+ "ft",
59
+ "port",
60
+ "lake",
61
+ "mount",
62
+ "mt",
63
+ "north",
64
+ "south",
65
+ "east",
66
+ "west",
67
+ "upper",
68
+ "lower",
69
+ "great",
70
+ "little",
71
+ "saint",
72
+ "st",
73
+ "st.",
74
+ // Romance
75
+ "san",
76
+ "santa",
77
+ "santo",
78
+ "são",
79
+ "sao",
80
+ "los",
81
+ "las",
82
+ "el",
83
+ "la",
84
+ "le",
85
+ "les",
86
+ "villa",
87
+ "rio",
88
+ "nueva",
89
+ "nuevo",
90
+ "puerto",
91
+ "ciudad",
92
+ "campo",
93
+ "monte",
94
+ "castel",
95
+ "borgo",
96
+ // Germanic / Nordic
97
+ "bad",
98
+ "sankt",
99
+ "neu",
100
+ "alt",
101
+ "groß",
102
+ "gross",
103
+ "klein",
104
+ "ober",
105
+ "unter",
106
+ "nieuw",
107
+ "oud",
108
+ "ny",
109
+ "stor",
110
+ "lille",
111
+ "sint",
112
+ // Definite article as a head — "The Valley" (Anguilla), "The Hague", "The Bottom".
113
+ "the",
114
+ // Generic toponymic heads outside the Latin/Germanic families, each a common
115
+ // noun in its own language — Semitic "tel" (mound), Malay "kuala" (confluence),
116
+ // Khmer "phnom" (hill) — that heads a place name the way "mount" does.
117
+ // `mailwoman/test/kind-intent-invariance.test.ts` covers them.
118
+ "tel",
119
+ "kuala",
120
+ "phnom",
121
+ "cape",
122
+ "isle",
123
+ "isla",
124
+ "ilha",
125
+ ]);
126
+ /**
127
+ * Generic toponymic tail nouns — the other half of the same bounded morphological
128
+ * class, so a place name whose last token is a settlement/landform generic
129
+ * ("Belize City", "George Town", "Palm Springs") is one name rather than two.
130
+ */
131
+ const TOPONYM_TAIL_NOUNS = new Set([
132
+ "city",
133
+ "town",
134
+ "ville",
135
+ "village",
136
+ "borough",
137
+ "springs",
138
+ "falls",
139
+ "beach",
140
+ "heights",
141
+ "valley",
142
+ "island",
143
+ "islands",
144
+ "bay",
145
+ "harbour",
146
+ "harbor",
147
+ "park",
148
+ "hills",
149
+ "river",
150
+ "creek",
151
+ "point",
152
+ "stadt",
153
+ "burg",
154
+ ]);
155
+ /**
156
+ * Deictic locator tails — "near me", "nearby", "around here", "in my area" —
157
+ * the bounded class `preposition + a reference to the asker`, where `me`, `here`,
158
+ * `my <noun>` are function words rather than places.
159
+ *
160
+ * Anchored to the end of the string (`$`) on purpose: the query names no anchor,
161
+ * so anything after the locator is an anchor and disqualifies it.
162
+ *
163
+ * Linear by construction: every alternative begins with a required literal and the only
164
+ * quantifiers are bounded `\s+` runs between two required literals or trailing before `$`,
165
+ * with no unbounded-whitespace-then-literal prefix (the `js/polynomial-redos` shape).
166
+ * `ANCHOR_SEPARATOR` in `poi.ts` uses the same analysis.
167
+ */
168
+ const DEICTIC_LOCATOR_TAIL = /\b(?:near|close\s+to|next\s+to|around|by|closest\s+to|nearest\s+to)\s+(?:me|us|here|my\s+(?:location|position|area|place|house|home))\s*$/;
169
+ /**
170
+ * The adverbial half of the same class, where the deixis is baked into the word
171
+ * rather than introduced by a preposition.
172
+ */
173
+ const DEICTIC_ADVERB_TAIL = /\b(?:nearby|near\s?by|close\s+by|around\s+here|in\s+my\s+(?:area|neighborhood|neighbourhood))\s*$/;
174
+ function hasDeicticTail(lowercased) {
175
+ return DEICTIC_LOCATOR_TAIL.test(lowercased) || DEICTIC_ADVERB_TAIL.test(lowercased);
176
+ }
177
+ /**
178
+ * The conditions `bare_toponym` and `route_pair` share: no address grammar of any kind,
179
+ * one segment, alpha throughout.
180
+ *
181
+ * @returns the word list when the input clears them, `null` when it does not.
182
+ * The conditions are deliberately a superset of `scoreLocalityOnly`'s, so `bare_toponym` is
183
+ * a strict refinement of `locality_only` and can never fire where `locality_only` did not.
184
+ */
185
+ function bareNameWords(input, shape) {
186
+ const text = input.normalized.trim();
187
+ if (!text || text.length > MAX_LOCALITY_ONLY_LENGTH)
188
+ return null;
189
+ // A recognized postcode/known format is address grammar, so no bare toponym survives it.
190
+ if (shape.knownFormats.length)
191
+ return null;
192
+ // `alpha` excludes every house number and postcode by construction, the cheapest statement of
193
+ // "no address grammar"; it is silent about whether a name is present, so the letter test stands
194
+ // beside it because `foldInputClass` answers `alpha` for input carrying no classified token.
195
+ if (shape.characterClass !== "alpha" || !carriesLetter(text))
196
+ return null;
197
+ // A comma is the admin-context marker ("Paris, FR"), so one segment or the name is not bare.
198
+ if ((shape.segments?.length ?? 1) !== 1)
199
+ return null;
200
+ const lowercased = text.toLowerCase();
201
+ if (hasDeicticTail(lowercased))
202
+ return null;
203
+ const words = wordsOf(text);
204
+ if (!words.length || words.length > MAX_BARE_TOPONYM_WORDS)
205
+ return null;
206
+ for (const word of words) {
207
+ if (isDisqualifyingStreetSuffix(word))
208
+ return null;
209
+ }
210
+ return words;
211
+ }
212
+ /**
213
+ * `bare_toponym` rule: a single coherent place-name carrying no address grammar, feeding the
214
+ * declared-ambiguity path without asserting which place — that is the resolver's question,
215
+ * decided by the answer's dominance margin in `mailwoman/query-intent.ts`.
216
+ */
217
+ export function scoreBareToponym(input, shape) {
218
+ return bareNameWords(input, shape) ? BARE_TOPONYM_CONFIDENCE : 0;
219
+ }
220
+ /**
221
+ * `route_pair` rule: exactly two toponym-shaped tokens with no token between them.
222
+ *
223
+ * The known confound is structural and unfixable here.
224
+ * "Paris London" and "Moscow Idaho" have the same string shape.
225
+ *
226
+ * A classifier needs gazetteer knowledge that Idaho is a region to distinguish them.
227
+ * ROAD_TO_V9 §4.3 therefore specifies classification plus a declared fork.
228
+ *
229
+ * Both readings appear in the marker, neither wins and the resolver keeps its existing answer.
230
+ *
231
+ * The structurally separable class is a two-token single name ("New York", "Fort Worth")
232
+ * with a toponymic head particle.
233
+ * That guard keeps the fork off the common case.
234
+ */
235
+ export function scoreRoutePair(input, shape) {
236
+ const words = bareNameWords(input, shape);
237
+ if (!words || words.length !== 2)
238
+ return 0;
239
+ const [first, second] = [words[0].toLowerCase(), words[1].toLowerCase()];
240
+ // Reduplication — "Pago Pago", "Baden-Baden", "Walla Walla" — is a universal single-name
241
+ // signal that needs no lexicon, because a route from a place to itself is invalid.
242
+ if (first === second)
243
+ return 0;
244
+ if (TOPONYM_HEAD_PARTICLES.has(first) || TOPONYM_HEAD_PARTICLES.has(second))
245
+ return 0;
246
+ if (TOPONYM_TAIL_NOUNS.has(second))
247
+ return 0;
248
+ return ROUTE_PAIR_CONFIDENCE;
249
+ }
250
+ /**
251
+ * `near_me` rule: a subject plus a deictic locator, with no anchor.
252
+ *
253
+ * A non-empty subject is required, so a bare "near me" stays with the `landmark` leaders rule.
254
+ */
255
+ export function scoreNearMe(input, _shape) {
256
+ const lowercased = input.normalized.trim().toLowerCase();
257
+ if (!hasDeicticTail(lowercased))
258
+ return 0;
259
+ // The subject is everything before the locator.
260
+ // `hasDeicticTail` already anchored the match to the end, so the first match
261
+ // index marks where the subject stops.
262
+ const match = DEICTIC_LOCATOR_TAIL.exec(lowercased) ?? DEICTIC_ADVERB_TAIL.exec(lowercased);
263
+ if (!match)
264
+ return 0;
265
+ return lowercased.slice(0, match.index).trim() ? NEAR_ME_CONFIDENCE : 0;
266
+ }
267
+ /**
268
+ * The subject of a `near_me` query is the requested category or thing with the locator stripped.
269
+ *
270
+ * It is empty when the rule would not have fired.
271
+ * The subject builds marker evidence and never a route.
272
+ */
273
+ export function nearMeSubject(input) {
274
+ const trimmed = input.normalized.trim();
275
+ const lowercased = trimmed.toLowerCase();
276
+ const match = DEICTIC_LOCATOR_TAIL.exec(lowercased) ?? DEICTIC_ADVERB_TAIL.exec(lowercased);
277
+ if (!match)
278
+ return "";
279
+ return trimmed.slice(0, match.index).trim();
280
+ }
281
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../lib/intent/rules.ts"],"names":[],"mappings":"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"}