cry-search 2.0.1 → 2.0.3

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/CLAUDE.md CHANGED
@@ -433,3 +433,23 @@ src/
433
433
  1. Memory: numeric ~45% less
434
434
  2. Build time: ~same
435
435
  3. Search time: varies by match mode
436
+
437
+ ## Phone number normalization
438
+
439
+ - `DEFAULT_PHONE_FIELDS`: `telefon`, `phone`, `gsm`, `msisdn`.
440
+ - `SearchableObjectSpec.phoneFields` compares exact field paths; configure
441
+ nested values such as `kontakt.telefon` explicitly. `[]` preserves legacy
442
+ tokenization.
443
+ - Phone values are converted directly to one ASCII digit token and encoded with
444
+ `phoneMatchMode` (default `anywhere`); they do not pass through the ordinary
445
+ date/preprocess/sanitize/tokenize pipeline.
446
+ - `phoneNormalizer` returns ASCII digits or `undefined`. Invalid non-digit
447
+ custom output throws at indexing time.
448
+ - A complete phone-like query gets canonical phone variants before raw tokens;
449
+ if a phone variant has candidates, raw matching is only a fallback.
450
+ - `phoneQueryNormalizer` overrides query recognition. `SearchUniverse` derives
451
+ its normalizers from collection specs; direct custom search passes
452
+ `resolvePhoneQueryNormalizer(spec)` in `SearchOpts.phoneQueryNormalizers`.
453
+ - `extractSearchableStringFn` is an explicit escape hatch and bypasses automatic
454
+ phone-field processing. Phone fields are excluded from `keyFields` positional
455
+ ranking.
package/README.md CHANGED
@@ -450,6 +450,39 @@ Match mode for numbers is determined by the **target token's length** (not the q
450
450
  // Use "=12" for exact match on short numbers
451
451
  ```
452
452
 
453
+ ### Phone Number Normalization
454
+
455
+ Phone fields are indexed as one canonical digit token, so the following values
456
+ converge without adding a dependency on a country-specific phone library:
457
+
458
+ ```text
459
+ 041/123-456 → 041123456
460
+ 041 123 456 → 041123456
461
+ +386 41 123 456 → 38641123456
462
+ ```
463
+
464
+ `telefon`, `phone`, `gsm`, and `msisdn` are normalized by default. Configure
465
+ additional nested paths explicitly, or opt out per collection:
466
+
467
+ ```typescript
468
+ const spec = {
469
+ searchInFields: ['name', 'kontakt.telefon'],
470
+ phoneFields: ['kontakt.telefon'],
471
+ // phoneFields: [], // preserve legacy segment tokenization
472
+ };
473
+ ```
474
+
475
+ A complete phone-like query is normalized before the ordinary tokenized query.
476
+ Phone results take priority; raw tokens are used only when no phone variant
477
+ matches. This keeps `041 123 456` precise while preserving raw-code search as a
478
+ fallback. Mixed text such as `Janez 041/123-456` is intentionally not parsed
479
+ as a phone query.
480
+
481
+ For custom canonicalization, configure `phoneNormalizer`. `SearchUniverse`
482
+ uses its matching query normalizer automatically. A direct `searchInData*`
483
+ caller must pass `resolvePhoneQueryNormalizer(spec)` via
484
+ `phoneQueryNormalizers` when using a custom normalizer.
485
+
453
486
  ### Hyphen Splitting
454
487
 
455
488
  Tokens are split at letter-dash-digit, digit-dash-letter and letter-dash-letter boundaries during preprocessing. Digit-dash-digit ranges (e.g., `12-25`) are preserved.
package/UNIVERSE.md CHANGED
@@ -862,3 +862,16 @@ For direct metadata manipulation without SearchUniverse:
862
862
  │ │
863
863
  └─────────────────────────────────────────────────────────────────────────────┘
864
864
  ```
865
+
866
+ ## Phone number normalization
867
+
868
+ `SearchUniverse.searchUniversally` derives a phone query normalizer from each
869
+ collection's `spec`. A standalone collection uses its own normalizer. A linked
870
+ search evaluates normalizers from both the primary and linked specs, so a
871
+ custom primary phone format remains searchable through a linked collection.
872
+
873
+ For a complete phone-like query, matching canonical phone variants takes
874
+ priority over the ordinary segmented query. If no phone variant finds a result,
875
+ SearchUniverse falls back to its usual raw-token search. Caller-supplied
876
+ `phoneQueryNormalizers` are additional variants; they do not replace the
877
+ collection-derived normalizers.
@@ -1 +1 @@
1
- {"version":3,"file":"SearchUniverse.d.ts","sourceRoot":"","sources":["../../src/common/SearchUniverse.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SearchUniverse.d.ts","sourceRoot":"","sources":["../../src/common/SearchUniverse.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"createSearchArrayMetadata.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/createSearchArrayMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACrB,MAAM,aAAa,CAAC;AASrB;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAEjE;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMzD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAS1D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMrD,CAAC;AAKF,mDAAmD;AACnD,eAAO,MAAM,aAAa,QAAwB,CAAC;AAEnD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAE5E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAS1D;AAoED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,MAAM,EAAE,wBAAwB,GAAG,SAAS,CAAC;CAC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,SAAS,gBAAgB,EAAE,CAAC,GAAG,OAAO,EACnF,GAAG,EAAE,CAAC,EACN,IAAI,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,EAC5C,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,oBAAoB,GAAG,SAAS,CAoBlC;AA6GD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gCAAgC,CAAC,CAAC,SAAS,gBAAgB,EAAE,CAAC,GAAG,OAAO,EACtF,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,CAAC,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAChC,qBAAqB,CAoBvB;AAED,yDAAyD;AACzD,wBAAgB,0BAA0B,IAAI,qBAAqB,CAElE"}
1
+ {"version":3,"file":"createSearchArrayMetadata.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/createSearchArrayMetadata.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACrB,MAAM,aAAa,CAAC;AAUrB;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAEjE;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMzD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAS1D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMrD,CAAC;AAKF,mDAAmD;AACnD,eAAO,MAAM,aAAa,QAAwB,CAAC;AAEnD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAE5E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAS1D;AAoED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,MAAM,EAAE,wBAAwB,GAAG,SAAS,CAAC;CAC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,SAAS,gBAAgB,EAAE,CAAC,GAAG,OAAO,EACnF,GAAG,EAAE,CAAC,EACN,IAAI,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,EAC5C,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,oBAAoB,GAAG,SAAS,CA0BlC;AAgID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gCAAgC,CAAC,CAAC,SAAS,gBAAgB,EAAE,CAAC,GAAG,OAAO,EACtF,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,CAAC,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAChC,qBAAqB,CAoBvB;AAED,yDAAyD;AACzD,wBAAgB,0BAA0B,IAAI,qBAAqB,CAElE"}
@@ -1,31 +1,9 @@
1
1
  import type { SearchableObject, SearchOpts, LinkedSearchResult, LinkedSearchResultWithMeta } from '../../types';
2
2
  import type { NumericLinkedSearchMetadataWithData } from './createSearchLinkedMetadata';
3
3
  /**
4
- * Searches across linked data sets using numeric tokens, ranked by positional relevance.
4
+ * Searches linked metadata with canonical phone variants before raw query tokens.
5
5
  *
6
- * For each primary item, collects all tokens from the primary and its linked items,
7
- * then matches query tokens against this combined pool. Returns primary items with
8
- * only the linked items that contributed to the match.
9
- *
10
- * **Ranking:** results are sorted by positional score (using `metadata.unsorted`
11
- * of primary + matched linked items, in source order). Items with `keyFields`
12
- * declared at build time get accurate positional scoring; items without retain
13
- * primary insertion order via the deterministic tiebreaker.
14
- *
15
- * **Key behaviors:**
16
- * - Tokens that match in the primary are excluded from linked matching (prevents
17
- * foreign key redundancy)
18
- * - Only linked items that match ALL remaining tokens are included in results
19
- * - `matchedIn` indicates where the match was found: 'primary', 'linked', or 'both'
20
- *
21
- * @typeParam TPrimary - Type of primary items (e.g., Stranka)
22
- * @typeParam TLinked - Type of linked items (e.g., Pacient)
23
- *
24
- * @param query - The search query string (will be tokenized)
25
- * @param linked - Linked metadata from {@link createSearchLinkedMetadataNumeric}
26
- * @param limit - Maximum number of results to return (default: 10)
27
- * @param opts - Optional search options for controlling match behavior
28
- * @returns Search results with query tokens, matching items, and hasNegation flag
6
+ * @feature [phone-search]
29
7
  */
30
8
  export declare function findInLinkedArraysNumeric<TPrimary extends SearchableObject, TLinked extends SearchableObject>(query: string, linked: NumericLinkedSearchMetadataWithData<TPrimary, TLinked>, limit?: number, opts?: SearchOpts): LinkedSearchResultWithMeta<TPrimary, TLinked>;
31
9
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"findInLinkedArrays.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/findInLinkedArrays.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,0BAA0B,EAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAC;AAUxF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,SAAS,gBAAgB,EACjC,OAAO,SAAS,gBAAgB,EAEhC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,mCAAmC,CAAC,QAAQ,EAAE,OAAO,CAAC,EAC9D,KAAK,GAAE,MAAW,EAClB,IAAI,CAAC,EAAE,UAAU,GAChB,0BAA0B,CAAC,QAAQ,EAAE,OAAO,CAAC,CAmO/C;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,SAAS,gBAAgB,EACjC,OAAO,SAAS,gBAAgB,EAEhC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,mCAAmC,CAAC,QAAQ,EAAE,OAAO,CAAC,EAC9D,KAAK,GAAE,MAAW,EAClB,IAAI,CAAC,EAAE,UAAU,GAChB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAEzC"}
1
+ {"version":3,"file":"findInLinkedArrays.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/findInLinkedArrays.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,0BAA0B,EAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAC;AA6QxF;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,SAAS,gBAAgB,EACjC,OAAO,SAAS,gBAAgB,EAEhC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,mCAAmC,CAAC,QAAQ,EAAE,OAAO,CAAC,EAC9D,KAAK,GAAE,MAAW,EAClB,IAAI,CAAC,EAAE,UAAU,GAChB,0BAA0B,CAAC,QAAQ,EAAE,OAAO,CAAC,CAoC/C;AAiCD;;GAEG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,SAAS,gBAAgB,EACjC,OAAO,SAAS,gBAAgB,EAEhC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,mCAAmC,CAAC,QAAQ,EAAE,OAAO,CAAC,EAC9D,KAAK,GAAE,MAAW,EAClB,IAAI,CAAC,EAAE,UAAU,GAChB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAEzC"}
@@ -0,0 +1,15 @@
1
+ import type { PreparedQueryToken } from '../../utils/matchTokenNumeric';
2
+ import type { SearchOpts } from '../../types';
3
+ /** Prepared raw and canonical-phone alternatives for one search query. */
4
+ export interface PreparedQueryVariants {
5
+ rawTokenStrs: string[];
6
+ rawTokens: PreparedQueryToken[];
7
+ phoneTokens: PreparedQueryToken[][];
8
+ }
9
+ /**
10
+ * Produces deduplicated phone alternatives without changing ordinary query tokens.
11
+ *
12
+ * @feature [phone-search]
13
+ */
14
+ export declare function prepareQueryVariants(query: string, opts?: SearchOpts): PreparedQueryVariants;
15
+ //# sourceMappingURL=prepareQueryVariants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepareQueryVariants.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/prepareQueryVariants.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAIxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChC,WAAW,EAAE,kBAAkB,EAAE,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,qBAAqB,CAmB5F"}
@@ -1 +1 @@
1
- {"version":3,"file":"searchInData.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/searchInData.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,EAAE,EACF,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACX,MAAM,aAAa,CAAC;AAMrB;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,qBAAqB,EAC/B,IAAI,CAAC,EAAE,UAAU,GAChB,EAAE,EAAE,CAEN;AAED;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAAC,CAAC,SAAS,gBAAgB,EACzE,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,qBAAqB,EAC/B,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,CAAC,EAAE,UAAU,GAChB,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAYZ;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,qBAAqB,EAC/B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,UAAU,GAChB,EAAE,EAAE,CAiDN"}
1
+ {"version":3,"file":"searchInData.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/searchInData.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,EAAE,EACF,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACX,MAAM,aAAa,CAAC;AAMrB;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,qBAAqB,EAC/B,IAAI,CAAC,EAAE,UAAU,GAChB,EAAE,EAAE,CAEN;AAED;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAAC,CAAC,SAAS,gBAAgB,EACzE,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,qBAAqB,EAC/B,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,CAAC,EAAE,UAAU,GAChB,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAYZ;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,qBAAqB,EAC/B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,UAAU,GAChB,EAAE,EAAE,CA4CN"}
package/dist/index.cjs CHANGED
@@ -2,27 +2,37 @@ var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __moduleCache = /* @__PURE__ */ new WeakMap;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
6
8
  var __toCommonJS = (from) => {
7
- var entry = __moduleCache.get(from), desc;
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
8
10
  if (entry)
9
11
  return entry;
10
12
  entry = __defProp({}, "__esModule", { value: true });
11
- if (from && typeof from === "object" || typeof from === "function")
12
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
- get: () => from[key],
14
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
- }));
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
16
21
  __moduleCache.set(from, entry);
17
22
  return entry;
18
23
  };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
19
29
  var __export = (target, all) => {
20
30
  for (var name in all)
21
31
  __defProp(target, name, {
22
32
  get: all[name],
23
33
  enumerable: true,
24
34
  configurable: true,
25
- set: (newValue) => all[name] = () => newValue
35
+ set: __exportSetter.bind(all, name)
26
36
  });
27
37
  };
28
38
 
@@ -49,6 +59,7 @@ __export(exports_src, {
49
59
  scoreOne: () => scoreOne,
50
60
  scoreBag: () => scoreBag,
51
61
  sanitiseString: () => sanitiseString,
62
+ resolvePhoneQueryNormalizer: () => resolvePhoneQueryNormalizer,
52
63
  resetTokenRegistry: () => resetTokenRegistry,
53
64
  removePrimaryItem: () => removePrimaryItemNumeric,
54
65
  removeLinkedItem: () => removeLinkedItemNumeric,
@@ -71,11 +82,14 @@ __export(exports_src, {
71
82
  findInLinkedArraysSimpleNumeric: () => findInLinkedArraysSimpleNumeric,
72
83
  findInLinkedArraysNumeric: () => findInLinkedArraysNumeric,
73
84
  encodeTokenWithMode: () => encodeTokenWithMode,
85
+ defaultQueryPhoneNormalizer: () => defaultQueryPhoneNormalizer,
86
+ defaultPhoneNormalizer: () => defaultPhoneNormalizer,
74
87
  decodeTokenId: () => decodeTokenId,
75
88
  decodeMatchMode: () => decodeMatchMode,
76
89
  createTokensMap: () => createTokensMap,
77
90
  createSearchLinkedMetadata: () => createSearchLinkedMetadataNumeric,
78
91
  createSearchArrayMetadata: () => createSearchArrayMetadataNumeric,
92
+ compareTokenized: () => compareTokenized,
79
93
  clearTokensMap: () => clearTokensMap,
80
94
  binarySearchUint32: () => binarySearchUint32,
81
95
  arrayToSearchableData: () => arrayToSearchableData,
@@ -87,11 +101,13 @@ __export(exports_src, {
87
101
  MATCH_MODE_PREFIXES: () => MATCH_MODE_PREFIXES,
88
102
  MATCH_MODE_BITS: () => MATCH_MODE_BITS,
89
103
  DEFAULT_SEARCH_OPTS: () => DEFAULT_SEARCH_OPTS,
104
+ DEFAULT_PHONE_FIELDS: () => DEFAULT_PHONE_FIELDS,
90
105
  AugmentedMetadata: () => AugmentedMetadata
91
106
  });
92
107
  module.exports = __toCommonJS(exports_src);
93
108
 
94
109
  // src/types/index.ts
110
+ var DEFAULT_PHONE_FIELDS = ["telefon", "phone", "gsm", "msisdn"];
95
111
  var DEFAULT_FIRST_TOKENS_N = 8;
96
112
  var DEFAULT_SEARCH_OPTS = {
97
113
  matchWords: "start",
@@ -304,6 +320,53 @@ function prepareStringForSearch(input) {
304
320
  const tokens = tokenize(sanitised, DEFAULT_TOKENIZE_CONFIG);
305
321
  return tokens.sort();
306
322
  }
323
+ // src/utils/compareTokenized.ts
324
+ function compareTokenized(a, b) {
325
+ const aTokens = new Set(prepareStringForSearch(a));
326
+ const bTokens = new Set(prepareStringForSearch(b));
327
+ if (aTokens.size === 0 && bTokens.size === 0)
328
+ return 1;
329
+ let intersection = 0;
330
+ for (const token of aTokens) {
331
+ if (bTokens.has(token))
332
+ intersection++;
333
+ }
334
+ const union = aTokens.size + bTokens.size - intersection;
335
+ if (union === 0)
336
+ return 0;
337
+ return intersection / union;
338
+ }
339
+ // src/utils/phoneNormalization.ts
340
+ var RE_ASCII_DIGITS = /^[0-9]+$/;
341
+ var RE_PHONE_LIKE_QUERY = /^[\d+()/\-. ]+$/;
342
+ var MIN_PHONE_DIGITS = 4;
343
+ var defaultPhoneNormalizer = (value) => {
344
+ const digits = value.replace(/\D/g, "");
345
+ return digits.length >= MIN_PHONE_DIGITS ? digits : undefined;
346
+ };
347
+ var defaultQueryPhoneNormalizer = (query) => {
348
+ if (!RE_PHONE_LIKE_QUERY.test(query))
349
+ return;
350
+ return defaultPhoneNormalizer(query);
351
+ };
352
+ function validateNormalizedPhone(normalized, normalizerName) {
353
+ if (normalized === undefined)
354
+ return;
355
+ if (typeof normalized !== "string" || !RE_ASCII_DIGITS.test(normalized)) {
356
+ throw new Error(`${normalizerName} must return an ASCII digit string or undefined`);
357
+ }
358
+ return normalized;
359
+ }
360
+ function resolvePhoneQueryNormalizer(spec) {
361
+ if (spec?.phoneQueryNormalizer)
362
+ return spec.phoneQueryNormalizer;
363
+ const phoneNormalizer = spec?.phoneNormalizer ?? defaultPhoneNormalizer;
364
+ return (query) => {
365
+ if (!RE_PHONE_LIKE_QUERY.test(query))
366
+ return;
367
+ return phoneNormalizer(query);
368
+ };
369
+ }
307
370
  // src/utils/tokenRegistry.ts
308
371
  var INITIAL_CAPACITY = 10 * 1000;
309
372
  var tokens = new Array(INITIAL_CAPACITY);
@@ -481,7 +544,7 @@ function prepareObjectForSearchNumeric(obj, spec, tokensMap) {
481
544
  const sorted = spec?.extractSearchableStringFn ? buildSortedFromExtractFn(obj, spec.extractSearchableStringFn, tokensMap) : buildSortedFromFields(obj, spec, tokensMap);
482
545
  if (sorted === undefined)
483
546
  return;
484
- const firstN = spec?.keyFields && spec.keyFields.length > 0 ? buildFirstNFromKeyFields(obj, spec.keyFields, spec.firstTokensN ?? DEFAULT_FIRST_TOKENS_N, tokensMap) : undefined;
547
+ const firstN = spec?.keyFields && spec.keyFields.length > 0 ? buildFirstNFromKeyFields(obj, spec.keyFields, spec.firstTokensN ?? DEFAULT_FIRST_TOKENS_N, spec.phoneFields ?? DEFAULT_PHONE_FIELDS, tokensMap) : undefined;
485
548
  return { sorted, firstN };
486
549
  }
487
550
  function buildSortedFromExtractFn(obj, extractFn, tokensMap) {
@@ -494,7 +557,9 @@ function buildSortedFromFields(obj, spec, tokensMap) {
494
557
  const allTokens = [];
495
558
  const fieldMatchModes = spec?.fieldMatchModes;
496
559
  const fieldsToSearch = spec?.searchInFields ?? Object.keys(obj);
497
- const fieldsToExclude = new Set(spec?.dontSearchInFields ?? []);
560
+ const fieldsToExclude = new Set((spec?.dontSearchInFields ?? []).map(String));
561
+ const phoneFields = new Set(spec?.phoneFields ?? DEFAULT_PHONE_FIELDS);
562
+ const phoneNormalizer = spec?.phoneNormalizer ?? defaultPhoneNormalizer;
498
563
  for (const key of fieldsToSearch) {
499
564
  const keyStr = key;
500
565
  if (!spec?.searchInFields && DEFAULT_EXCLUDED_FIELDS.has(keyStr))
@@ -502,36 +567,50 @@ function buildSortedFromFields(obj, spec, tokensMap) {
502
567
  if (fieldsToExclude.has(key))
503
568
  continue;
504
569
  const isDot = isDotNotation(keyStr);
505
- const value = isDot ? getValueByPath(obj, keyStr) : obj[key];
570
+ const value = isDot ? getValueByPath(obj, keyStr) : obj[keyStr];
506
571
  if (value == null)
507
572
  continue;
508
573
  const matchMode = fieldMatchModes?.[key];
509
574
  const fieldStrings = [];
510
575
  extractStringsFromValue(value, fieldStrings);
511
- if (fieldStrings.length > 0) {
512
- const fieldText = fieldStrings.join(" ");
513
- const datesNormalized = normalizeDates(fieldText);
514
- const preprocessed = preprocessString(datesNormalized);
515
- const sanitised = sanitiseString(preprocessed);
516
- const stringTokens = tokenize(sanitised, DEFAULT_TOKENIZE_CONFIG);
517
- const effectiveMode = matchMode ?? (keyStr === "_id" ? "whole" : undefined);
518
- for (const token of stringTokens) {
519
- const tokenId = getOrCreateTokenId(token, tokensMap);
520
- if (effectiveMode) {
521
- allTokens.push(encodeTokenWithMode(tokenId, effectiveMode));
522
- } else {
523
- allTokens.push(tokenId);
524
- }
576
+ if (phoneFields.has(keyStr)) {
577
+ const phoneMatchMode = spec?.phoneMatchMode ?? "anywhere";
578
+ for (const fieldString of fieldStrings) {
579
+ const normalized = validateNormalizedPhone(phoneNormalizer(fieldString), `Phone normalizer for field "${keyStr}"`);
580
+ if (normalized === undefined)
581
+ continue;
582
+ const tokenId = getOrCreateTokenId(normalized, tokensMap);
583
+ allTokens.push(encodeTokenWithMode(tokenId, phoneMatchMode));
584
+ }
585
+ continue;
586
+ }
587
+ if (fieldStrings.length === 0)
588
+ continue;
589
+ const fieldText = fieldStrings.join(" ");
590
+ const datesNormalized = normalizeDates(fieldText);
591
+ const preprocessed = preprocessString(datesNormalized);
592
+ const sanitised = sanitiseString(preprocessed);
593
+ const stringTokens = tokenize(sanitised, DEFAULT_TOKENIZE_CONFIG);
594
+ const effectiveMode = matchMode ?? (keyStr === "_id" ? "whole" : undefined);
595
+ for (const token of stringTokens) {
596
+ const tokenId = getOrCreateTokenId(token, tokensMap);
597
+ if (effectiveMode) {
598
+ allTokens.push(encodeTokenWithMode(tokenId, effectiveMode));
599
+ } else {
600
+ allTokens.push(tokenId);
525
601
  }
526
602
  }
527
603
  }
528
604
  const uniqueTokens = [...new Set(allTokens)].sort((a, b) => a - b);
529
605
  return new Uint32Array(uniqueTokens);
530
606
  }
531
- function buildFirstNFromKeyFields(obj, keyFields, n, tokensMap) {
607
+ function buildFirstNFromKeyFields(obj, keyFields, n, phoneFields, tokensMap) {
532
608
  const strings = [];
609
+ const phoneFieldPaths = new Set(phoneFields);
533
610
  for (const field of keyFields) {
534
611
  const keyStr = field;
612
+ if (phoneFieldPaths.has(keyStr))
613
+ continue;
535
614
  const value = isDotNotation(keyStr) ? getValueByPath(obj, keyStr) : obj[field];
536
615
  if (value == null)
537
616
  continue;
@@ -834,6 +913,23 @@ function allTokensMatchNumeric(searchTokenStrs, targetTokens, opts) {
834
913
  return allPreparedTokensMatch(preparedTokens, targetTokens, opts);
835
914
  }
836
915
 
916
+ // src/common/numeric/prepareQueryVariants.ts
917
+ function prepareQueryVariants(query, opts) {
918
+ const rawTokenStrs = prepareStringForSearch(query);
919
+ const rawTokens = prepareQueryTokens(rawTokenStrs);
920
+ const normalizers = opts?.phoneQueryNormalizers ?? [defaultQueryPhoneNormalizer];
921
+ const seenPhones = new Set;
922
+ const phoneTokens = [];
923
+ for (const normalizer of normalizers) {
924
+ const normalized = validateNormalizedPhone(normalizer(query), "Phone query normalizer");
925
+ if (normalized === undefined || seenPhones.has(normalized))
926
+ continue;
927
+ seenPhones.add(normalized);
928
+ phoneTokens.push(prepareQueryTokens([normalized]));
929
+ }
930
+ return { rawTokenStrs, rawTokens, phoneTokens };
931
+ }
932
+
837
933
  // src/common/numeric/scoring.ts
838
934
  function orderedTokenize(input) {
839
935
  return tokenize(sanitiseString(preprocessString(normalizeDates(input))), DEFAULT_TOKENIZE_CONFIG);
@@ -950,16 +1046,9 @@ function searchInDataReturnObjectsNumeric(query, metadata, data, opts) {
950
1046
  return results;
951
1047
  }
952
1048
  function searchInDataReturnIdsWithLimitNumeric(query, metadata, limit, opts) {
953
- const queryTokenStrs = prepareStringForSearch(query);
954
- if (queryTokenStrs.length === 0)
955
- return [];
956
- const preparedTokens = prepareQueryTokens(queryTokenStrs);
957
- const candidates = [];
958
- for (const [id, targetTokens] of metadata.sorted) {
959
- if (allPreparedTokensMatch(preparedTokens, targetTokens, opts)) {
960
- candidates.push(id);
961
- }
962
- }
1049
+ const { rawTokenStrs, rawTokens, phoneTokens } = prepareQueryVariants(query, opts);
1050
+ const phoneCandidates = findMatchingCandidates(metadata, phoneTokens, opts);
1051
+ const candidates = phoneCandidates.length > 0 ? phoneCandidates : findMatchingCandidates(metadata, rawTokens.length > 0 ? [rawTokens] : [], opts);
963
1052
  if (candidates.length === 0)
964
1053
  return [];
965
1054
  const qTokens = orderedTokenize(query);
@@ -994,13 +1083,22 @@ function searchInDataReturnIdsWithLimitNumeric(query, metadata, limit, opts) {
994
1083
  out.push(scored[i].id);
995
1084
  return out;
996
1085
  }
997
- // src/common/numeric/findInLinkedArrays.ts
998
- function findInLinkedArraysNumeric(query, linked, limit = 10, opts) {
999
- const queryTokenStrs = prepareStringForSearch(query);
1000
- if (queryTokenStrs.length === 0) {
1001
- return { queryTokens: queryTokenStrs, results: [], hasNegation: false };
1086
+ function findMatchingCandidates(metadata, variants, opts) {
1087
+ if (variants.length === 0)
1088
+ return [];
1089
+ const candidates = [];
1090
+ for (const [id, targetTokens] of metadata.sorted) {
1091
+ for (const preparedTokens of variants) {
1092
+ if (allPreparedTokensMatch(preparedTokens, targetTokens, opts)) {
1093
+ candidates.push(id);
1094
+ break;
1095
+ }
1096
+ }
1002
1097
  }
1003
- const preparedTokens = prepareQueryTokens(queryTokenStrs);
1098
+ return candidates;
1099
+ }
1100
+ // src/common/numeric/findInLinkedArrays.ts
1101
+ function findInLinkedArraysForPreparedTokens(query, queryTokenStrs, preparedTokens, linked, limit, opts) {
1004
1102
  const positiveTokens = preparedTokens.filter((t) => !t.negated);
1005
1103
  const negatedTokens = preparedTokens.filter((t) => t.negated);
1006
1104
  const scored = [];
@@ -1170,6 +1268,46 @@ function findInLinkedArraysNumeric(query, linked, limit = 10, opts) {
1170
1268
  results[i] = scored[i].result;
1171
1269
  return { queryTokens: queryTokenStrs, results, hasNegation: negatedTokens.length > 0 };
1172
1270
  }
1271
+ function findInLinkedArraysNumeric(query, linked, limit = 10, opts) {
1272
+ const { rawTokenStrs, rawTokens, phoneTokens } = prepareQueryVariants(query, opts);
1273
+ const phoneResults = [];
1274
+ for (const preparedTokens of phoneTokens) {
1275
+ const result = findInLinkedArraysForPreparedTokens(query, rawTokenStrs, preparedTokens, linked, Infinity, opts);
1276
+ phoneResults.push(...result.results);
1277
+ }
1278
+ if (phoneResults.length > 0) {
1279
+ return {
1280
+ queryTokens: rawTokenStrs,
1281
+ results: mergePhoneResults(phoneResults).slice(0, limit),
1282
+ hasNegation: false
1283
+ };
1284
+ }
1285
+ if (rawTokens.length === 0) {
1286
+ return { queryTokens: rawTokenStrs, results: [], hasNegation: false };
1287
+ }
1288
+ return findInLinkedArraysForPreparedTokens(query, rawTokenStrs, rawTokens, linked, limit, opts);
1289
+ }
1290
+ function mergePhoneResults(results) {
1291
+ const byPrimaryId = new Map;
1292
+ for (const result of results) {
1293
+ const existing = byPrimaryId.get(result.primary._id);
1294
+ if (existing === undefined) {
1295
+ byPrimaryId.set(result.primary._id, result);
1296
+ continue;
1297
+ }
1298
+ const linkedById = new Map(existing.linked.map((item) => [item._id, item]));
1299
+ for (const item of result.linked)
1300
+ linkedById.set(item._id, item);
1301
+ existing.linked = Array.from(linkedById.values());
1302
+ existing.matchedIn = mergeMatchLocation(existing.matchedIn, result.matchedIn);
1303
+ }
1304
+ return Array.from(byPrimaryId.values());
1305
+ }
1306
+ function mergeMatchLocation(first, second) {
1307
+ if (first === "both" || second === "both" || first !== second)
1308
+ return "both";
1309
+ return first;
1310
+ }
1173
1311
  function findInLinkedArraysSimpleNumeric(query, linked, limit = 10, opts) {
1174
1312
  return findInLinkedArraysNumeric(query, linked, limit, opts).results;
1175
1313
  }
@@ -1683,6 +1821,15 @@ function firstNEqual3(a, b) {
1683
1821
  return false;
1684
1822
  return tokensEqual3(a, b);
1685
1823
  }
1824
+ function withCollectionPhoneNormalizers(opts, specs) {
1825
+ return {
1826
+ ...opts,
1827
+ phoneQueryNormalizers: [
1828
+ ...specs.map((spec) => resolvePhoneQueryNormalizer(spec)),
1829
+ ...opts.phoneQueryNormalizers ?? []
1830
+ ]
1831
+ };
1832
+ }
1686
1833
 
1687
1834
  class SearchUniverse {
1688
1835
  collections = new Map;
@@ -2043,7 +2190,11 @@ class SearchUniverse {
2043
2190
  primaryData: primaryCollection.data,
2044
2191
  linkedData: collection.data
2045
2192
  };
2046
- const linkedSearchResults = findInLinkedArraysNumeric(query, linkedMetadata, limit, searchOpts);
2193
+ const linkedSearchOpts = withCollectionPhoneNormalizers(searchOpts, [
2194
+ primaryCollection.config.spec,
2195
+ collection.config.spec
2196
+ ]);
2197
+ const linkedSearchResults = findInLinkedArraysNumeric(query, linkedMetadata, limit, linkedSearchOpts);
2047
2198
  if (linkedSearchResults.results.length > 0) {
2048
2199
  const combinedResults = [];
2049
2200
  const hasNegation = linkedSearchResults.hasNegation;
@@ -2066,7 +2217,7 @@ class SearchUniverse {
2066
2217
  if (inCollections && !inCollections.includes(name)) {
2067
2218
  continue;
2068
2219
  }
2069
- const matchingIds = searchInDataReturnIdsWithLimitNumeric(query, collection.metadata, limit, searchOpts);
2220
+ const matchingIds = searchInDataReturnIdsWithLimitNumeric(query, collection.metadata, limit, withCollectionPhoneNormalizers(searchOpts, [collection.config.spec]));
2070
2221
  if (matchingIds.length > 0) {
2071
2222
  const collectionResults = [];
2072
2223
  for (const id of matchingIds) {
package/dist/index.d.cts CHANGED
@@ -2,15 +2,17 @@
2
2
  * cry-search - A fast, memory-efficient search library for large datasets
3
3
  * @packageDocumentation
4
4
  */
5
- export type { Id, SearchToken, SearchTokenSortedList, SearchableData, NumericToken, NumericTokenSortedList, NumericSearchMetadata, SearchableObject, MatchMode, SearchableObjectSpec, SearchOpts, SearchableLinkedObjectsSpec, LinkedSearchResult, LinkedSearchResultWithMeta, SearchResultWithMeta, SyncResult, SyncOptions, SyncLinkedResult, UpsertResult, UpdateSearchMetadataBatchItem, UniversalSearchOpts, CollectionConfig, UpdateResult, BatchUpdateResult, } from './types';
5
+ export type { Id, SearchToken, SearchTokenSortedList, SearchableData, NumericToken, NumericTokenSortedList, NumericSearchMetadata, SearchableObject, MatchMode, PhoneNormalizer, PhoneQueryNormalizer, SearchableObjectSpec, SearchOpts, SearchableLinkedObjectsSpec, LinkedSearchResult, LinkedSearchResultWithMeta, SearchResultWithMeta, SyncResult, SyncOptions, SyncLinkedResult, UpsertResult, UpdateSearchMetadataBatchItem, UniversalSearchOpts, CollectionConfig, UpdateResult, BatchUpdateResult, } from './types';
6
6
  export type { NumericSearchMetadata as SearchMetadata } from './types';
7
- export { DEFAULT_SEARCH_OPTS } from './types';
7
+ export { DEFAULT_SEARCH_OPTS, DEFAULT_PHONE_FIELDS } from './types';
8
8
  export { arrayToSearchableData, searchableDataToArray } from './types';
9
9
  export { prepareStringForSearch } from './utils/prepareStringForSearch';
10
10
  export { preprocessString } from './utils/preprocessString';
11
11
  export { sanitiseString } from './utils/sanitiseString';
12
12
  export { tokenize } from './utils/tokenize';
13
13
  export { normalizeDates } from './utils/normalizeDates';
14
+ export { compareTokenized } from './utils/compareTokenized';
15
+ export { defaultPhoneNormalizer, defaultQueryPhoneNormalizer, resolvePhoneQueryNormalizer, } from './utils/phoneNormalization';
14
16
  export { tokens, getTokenCount, getTokenById, findTokenId, registerToken, getOrCreateTokenId, createTokensMap, clearTokensMap, resetTokenRegistry, } from './utils/tokenRegistry';
15
17
  export { createSearchArrayMetadataNumeric as createSearchArrayMetadata, prepareObjectForSearchNumeric as prepareObjectForSearch, encodeTokenWithMode, decodeTokenId, decodeMatchMode, TOKEN_ID_MASK, MATCH_MODE_BITS, MATCH_MODE_PREFIXES, PREFIX_TO_MATCH_MODE, isDeletedOrBlocked, } from './common/numeric/createSearchArrayMetadata';
16
18
  export { createSearchLinkedMetadataNumeric as createSearchLinkedMetadata, type NumericLinkedSearchMetadataWithData, } from './common/numeric/createSearchLinkedMetadata';
package/dist/index.d.ts CHANGED
@@ -2,15 +2,17 @@
2
2
  * cry-search - A fast, memory-efficient search library for large datasets
3
3
  * @packageDocumentation
4
4
  */
5
- export type { Id, SearchToken, SearchTokenSortedList, SearchableData, NumericToken, NumericTokenSortedList, NumericSearchMetadata, SearchableObject, MatchMode, SearchableObjectSpec, SearchOpts, SearchableLinkedObjectsSpec, LinkedSearchResult, LinkedSearchResultWithMeta, SearchResultWithMeta, SyncResult, SyncOptions, SyncLinkedResult, UpsertResult, UpdateSearchMetadataBatchItem, UniversalSearchOpts, CollectionConfig, UpdateResult, BatchUpdateResult, } from './types';
5
+ export type { Id, SearchToken, SearchTokenSortedList, SearchableData, NumericToken, NumericTokenSortedList, NumericSearchMetadata, SearchableObject, MatchMode, PhoneNormalizer, PhoneQueryNormalizer, SearchableObjectSpec, SearchOpts, SearchableLinkedObjectsSpec, LinkedSearchResult, LinkedSearchResultWithMeta, SearchResultWithMeta, SyncResult, SyncOptions, SyncLinkedResult, UpsertResult, UpdateSearchMetadataBatchItem, UniversalSearchOpts, CollectionConfig, UpdateResult, BatchUpdateResult, } from './types';
6
6
  export type { NumericSearchMetadata as SearchMetadata } from './types';
7
- export { DEFAULT_SEARCH_OPTS } from './types';
7
+ export { DEFAULT_SEARCH_OPTS, DEFAULT_PHONE_FIELDS } from './types';
8
8
  export { arrayToSearchableData, searchableDataToArray } from './types';
9
9
  export { prepareStringForSearch } from './utils/prepareStringForSearch';
10
10
  export { preprocessString } from './utils/preprocessString';
11
11
  export { sanitiseString } from './utils/sanitiseString';
12
12
  export { tokenize } from './utils/tokenize';
13
13
  export { normalizeDates } from './utils/normalizeDates';
14
+ export { compareTokenized } from './utils/compareTokenized';
15
+ export { defaultPhoneNormalizer, defaultQueryPhoneNormalizer, resolvePhoneQueryNormalizer, } from './utils/phoneNormalization';
14
16
  export { tokens, getTokenCount, getTokenById, findTokenId, registerToken, getOrCreateTokenId, createTokensMap, clearTokensMap, resetTokenRegistry, } from './utils/tokenRegistry';
15
17
  export { createSearchArrayMetadataNumeric as createSearchArrayMetadata, prepareObjectForSearchNumeric as prepareObjectForSearch, encodeTokenWithMode, decodeTokenId, decodeMatchMode, TOKEN_ID_MASK, MATCH_MODE_BITS, MATCH_MODE_PREFIXES, PREFIX_TO_MATCH_MODE, isDeletedOrBlocked, } from './common/numeric/createSearchArrayMetadata';
16
18
  export { createSearchLinkedMetadataNumeric as createSearchLinkedMetadata, type NumericLinkedSearchMetadataWithData, } from './common/numeric/createSearchLinkedMetadata';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/types/index.ts
2
+ var DEFAULT_PHONE_FIELDS = ["telefon", "phone", "gsm", "msisdn"];
2
3
  var DEFAULT_FIRST_TOKENS_N = 8;
3
4
  var DEFAULT_SEARCH_OPTS = {
4
5
  matchWords: "start",
@@ -211,6 +212,53 @@ function prepareStringForSearch(input) {
211
212
  const tokens = tokenize(sanitised, DEFAULT_TOKENIZE_CONFIG);
212
213
  return tokens.sort();
213
214
  }
215
+ // src/utils/compareTokenized.ts
216
+ function compareTokenized(a, b) {
217
+ const aTokens = new Set(prepareStringForSearch(a));
218
+ const bTokens = new Set(prepareStringForSearch(b));
219
+ if (aTokens.size === 0 && bTokens.size === 0)
220
+ return 1;
221
+ let intersection = 0;
222
+ for (const token of aTokens) {
223
+ if (bTokens.has(token))
224
+ intersection++;
225
+ }
226
+ const union = aTokens.size + bTokens.size - intersection;
227
+ if (union === 0)
228
+ return 0;
229
+ return intersection / union;
230
+ }
231
+ // src/utils/phoneNormalization.ts
232
+ var RE_ASCII_DIGITS = /^[0-9]+$/;
233
+ var RE_PHONE_LIKE_QUERY = /^[\d+()/\-. ]+$/;
234
+ var MIN_PHONE_DIGITS = 4;
235
+ var defaultPhoneNormalizer = (value) => {
236
+ const digits = value.replace(/\D/g, "");
237
+ return digits.length >= MIN_PHONE_DIGITS ? digits : undefined;
238
+ };
239
+ var defaultQueryPhoneNormalizer = (query) => {
240
+ if (!RE_PHONE_LIKE_QUERY.test(query))
241
+ return;
242
+ return defaultPhoneNormalizer(query);
243
+ };
244
+ function validateNormalizedPhone(normalized, normalizerName) {
245
+ if (normalized === undefined)
246
+ return;
247
+ if (typeof normalized !== "string" || !RE_ASCII_DIGITS.test(normalized)) {
248
+ throw new Error(`${normalizerName} must return an ASCII digit string or undefined`);
249
+ }
250
+ return normalized;
251
+ }
252
+ function resolvePhoneQueryNormalizer(spec) {
253
+ if (spec?.phoneQueryNormalizer)
254
+ return spec.phoneQueryNormalizer;
255
+ const phoneNormalizer = spec?.phoneNormalizer ?? defaultPhoneNormalizer;
256
+ return (query) => {
257
+ if (!RE_PHONE_LIKE_QUERY.test(query))
258
+ return;
259
+ return phoneNormalizer(query);
260
+ };
261
+ }
214
262
  // src/utils/tokenRegistry.ts
215
263
  var INITIAL_CAPACITY = 10 * 1000;
216
264
  var tokens = new Array(INITIAL_CAPACITY);
@@ -388,7 +436,7 @@ function prepareObjectForSearchNumeric(obj, spec, tokensMap) {
388
436
  const sorted = spec?.extractSearchableStringFn ? buildSortedFromExtractFn(obj, spec.extractSearchableStringFn, tokensMap) : buildSortedFromFields(obj, spec, tokensMap);
389
437
  if (sorted === undefined)
390
438
  return;
391
- const firstN = spec?.keyFields && spec.keyFields.length > 0 ? buildFirstNFromKeyFields(obj, spec.keyFields, spec.firstTokensN ?? DEFAULT_FIRST_TOKENS_N, tokensMap) : undefined;
439
+ const firstN = spec?.keyFields && spec.keyFields.length > 0 ? buildFirstNFromKeyFields(obj, spec.keyFields, spec.firstTokensN ?? DEFAULT_FIRST_TOKENS_N, spec.phoneFields ?? DEFAULT_PHONE_FIELDS, tokensMap) : undefined;
392
440
  return { sorted, firstN };
393
441
  }
394
442
  function buildSortedFromExtractFn(obj, extractFn, tokensMap) {
@@ -401,7 +449,9 @@ function buildSortedFromFields(obj, spec, tokensMap) {
401
449
  const allTokens = [];
402
450
  const fieldMatchModes = spec?.fieldMatchModes;
403
451
  const fieldsToSearch = spec?.searchInFields ?? Object.keys(obj);
404
- const fieldsToExclude = new Set(spec?.dontSearchInFields ?? []);
452
+ const fieldsToExclude = new Set((spec?.dontSearchInFields ?? []).map(String));
453
+ const phoneFields = new Set(spec?.phoneFields ?? DEFAULT_PHONE_FIELDS);
454
+ const phoneNormalizer = spec?.phoneNormalizer ?? defaultPhoneNormalizer;
405
455
  for (const key of fieldsToSearch) {
406
456
  const keyStr = key;
407
457
  if (!spec?.searchInFields && DEFAULT_EXCLUDED_FIELDS.has(keyStr))
@@ -409,36 +459,50 @@ function buildSortedFromFields(obj, spec, tokensMap) {
409
459
  if (fieldsToExclude.has(key))
410
460
  continue;
411
461
  const isDot = isDotNotation(keyStr);
412
- const value = isDot ? getValueByPath(obj, keyStr) : obj[key];
462
+ const value = isDot ? getValueByPath(obj, keyStr) : obj[keyStr];
413
463
  if (value == null)
414
464
  continue;
415
465
  const matchMode = fieldMatchModes?.[key];
416
466
  const fieldStrings = [];
417
467
  extractStringsFromValue(value, fieldStrings);
418
- if (fieldStrings.length > 0) {
419
- const fieldText = fieldStrings.join(" ");
420
- const datesNormalized = normalizeDates(fieldText);
421
- const preprocessed = preprocessString(datesNormalized);
422
- const sanitised = sanitiseString(preprocessed);
423
- const stringTokens = tokenize(sanitised, DEFAULT_TOKENIZE_CONFIG);
424
- const effectiveMode = matchMode ?? (keyStr === "_id" ? "whole" : undefined);
425
- for (const token of stringTokens) {
426
- const tokenId = getOrCreateTokenId(token, tokensMap);
427
- if (effectiveMode) {
428
- allTokens.push(encodeTokenWithMode(tokenId, effectiveMode));
429
- } else {
430
- allTokens.push(tokenId);
431
- }
468
+ if (phoneFields.has(keyStr)) {
469
+ const phoneMatchMode = spec?.phoneMatchMode ?? "anywhere";
470
+ for (const fieldString of fieldStrings) {
471
+ const normalized = validateNormalizedPhone(phoneNormalizer(fieldString), `Phone normalizer for field "${keyStr}"`);
472
+ if (normalized === undefined)
473
+ continue;
474
+ const tokenId = getOrCreateTokenId(normalized, tokensMap);
475
+ allTokens.push(encodeTokenWithMode(tokenId, phoneMatchMode));
476
+ }
477
+ continue;
478
+ }
479
+ if (fieldStrings.length === 0)
480
+ continue;
481
+ const fieldText = fieldStrings.join(" ");
482
+ const datesNormalized = normalizeDates(fieldText);
483
+ const preprocessed = preprocessString(datesNormalized);
484
+ const sanitised = sanitiseString(preprocessed);
485
+ const stringTokens = tokenize(sanitised, DEFAULT_TOKENIZE_CONFIG);
486
+ const effectiveMode = matchMode ?? (keyStr === "_id" ? "whole" : undefined);
487
+ for (const token of stringTokens) {
488
+ const tokenId = getOrCreateTokenId(token, tokensMap);
489
+ if (effectiveMode) {
490
+ allTokens.push(encodeTokenWithMode(tokenId, effectiveMode));
491
+ } else {
492
+ allTokens.push(tokenId);
432
493
  }
433
494
  }
434
495
  }
435
496
  const uniqueTokens = [...new Set(allTokens)].sort((a, b) => a - b);
436
497
  return new Uint32Array(uniqueTokens);
437
498
  }
438
- function buildFirstNFromKeyFields(obj, keyFields, n, tokensMap) {
499
+ function buildFirstNFromKeyFields(obj, keyFields, n, phoneFields, tokensMap) {
439
500
  const strings = [];
501
+ const phoneFieldPaths = new Set(phoneFields);
440
502
  for (const field of keyFields) {
441
503
  const keyStr = field;
504
+ if (phoneFieldPaths.has(keyStr))
505
+ continue;
442
506
  const value = isDotNotation(keyStr) ? getValueByPath(obj, keyStr) : obj[field];
443
507
  if (value == null)
444
508
  continue;
@@ -741,6 +805,23 @@ function allTokensMatchNumeric(searchTokenStrs, targetTokens, opts) {
741
805
  return allPreparedTokensMatch(preparedTokens, targetTokens, opts);
742
806
  }
743
807
 
808
+ // src/common/numeric/prepareQueryVariants.ts
809
+ function prepareQueryVariants(query, opts) {
810
+ const rawTokenStrs = prepareStringForSearch(query);
811
+ const rawTokens = prepareQueryTokens(rawTokenStrs);
812
+ const normalizers = opts?.phoneQueryNormalizers ?? [defaultQueryPhoneNormalizer];
813
+ const seenPhones = new Set;
814
+ const phoneTokens = [];
815
+ for (const normalizer of normalizers) {
816
+ const normalized = validateNormalizedPhone(normalizer(query), "Phone query normalizer");
817
+ if (normalized === undefined || seenPhones.has(normalized))
818
+ continue;
819
+ seenPhones.add(normalized);
820
+ phoneTokens.push(prepareQueryTokens([normalized]));
821
+ }
822
+ return { rawTokenStrs, rawTokens, phoneTokens };
823
+ }
824
+
744
825
  // src/common/numeric/scoring.ts
745
826
  function orderedTokenize(input) {
746
827
  return tokenize(sanitiseString(preprocessString(normalizeDates(input))), DEFAULT_TOKENIZE_CONFIG);
@@ -857,16 +938,9 @@ function searchInDataReturnObjectsNumeric(query, metadata, data, opts) {
857
938
  return results;
858
939
  }
859
940
  function searchInDataReturnIdsWithLimitNumeric(query, metadata, limit, opts) {
860
- const queryTokenStrs = prepareStringForSearch(query);
861
- if (queryTokenStrs.length === 0)
862
- return [];
863
- const preparedTokens = prepareQueryTokens(queryTokenStrs);
864
- const candidates = [];
865
- for (const [id, targetTokens] of metadata.sorted) {
866
- if (allPreparedTokensMatch(preparedTokens, targetTokens, opts)) {
867
- candidates.push(id);
868
- }
869
- }
941
+ const { rawTokenStrs, rawTokens, phoneTokens } = prepareQueryVariants(query, opts);
942
+ const phoneCandidates = findMatchingCandidates(metadata, phoneTokens, opts);
943
+ const candidates = phoneCandidates.length > 0 ? phoneCandidates : findMatchingCandidates(metadata, rawTokens.length > 0 ? [rawTokens] : [], opts);
870
944
  if (candidates.length === 0)
871
945
  return [];
872
946
  const qTokens = orderedTokenize(query);
@@ -901,13 +975,22 @@ function searchInDataReturnIdsWithLimitNumeric(query, metadata, limit, opts) {
901
975
  out.push(scored[i].id);
902
976
  return out;
903
977
  }
904
- // src/common/numeric/findInLinkedArrays.ts
905
- function findInLinkedArraysNumeric(query, linked, limit = 10, opts) {
906
- const queryTokenStrs = prepareStringForSearch(query);
907
- if (queryTokenStrs.length === 0) {
908
- return { queryTokens: queryTokenStrs, results: [], hasNegation: false };
978
+ function findMatchingCandidates(metadata, variants, opts) {
979
+ if (variants.length === 0)
980
+ return [];
981
+ const candidates = [];
982
+ for (const [id, targetTokens] of metadata.sorted) {
983
+ for (const preparedTokens of variants) {
984
+ if (allPreparedTokensMatch(preparedTokens, targetTokens, opts)) {
985
+ candidates.push(id);
986
+ break;
987
+ }
988
+ }
909
989
  }
910
- const preparedTokens = prepareQueryTokens(queryTokenStrs);
990
+ return candidates;
991
+ }
992
+ // src/common/numeric/findInLinkedArrays.ts
993
+ function findInLinkedArraysForPreparedTokens(query, queryTokenStrs, preparedTokens, linked, limit, opts) {
911
994
  const positiveTokens = preparedTokens.filter((t) => !t.negated);
912
995
  const negatedTokens = preparedTokens.filter((t) => t.negated);
913
996
  const scored = [];
@@ -1077,6 +1160,46 @@ function findInLinkedArraysNumeric(query, linked, limit = 10, opts) {
1077
1160
  results[i] = scored[i].result;
1078
1161
  return { queryTokens: queryTokenStrs, results, hasNegation: negatedTokens.length > 0 };
1079
1162
  }
1163
+ function findInLinkedArraysNumeric(query, linked, limit = 10, opts) {
1164
+ const { rawTokenStrs, rawTokens, phoneTokens } = prepareQueryVariants(query, opts);
1165
+ const phoneResults = [];
1166
+ for (const preparedTokens of phoneTokens) {
1167
+ const result = findInLinkedArraysForPreparedTokens(query, rawTokenStrs, preparedTokens, linked, Infinity, opts);
1168
+ phoneResults.push(...result.results);
1169
+ }
1170
+ if (phoneResults.length > 0) {
1171
+ return {
1172
+ queryTokens: rawTokenStrs,
1173
+ results: mergePhoneResults(phoneResults).slice(0, limit),
1174
+ hasNegation: false
1175
+ };
1176
+ }
1177
+ if (rawTokens.length === 0) {
1178
+ return { queryTokens: rawTokenStrs, results: [], hasNegation: false };
1179
+ }
1180
+ return findInLinkedArraysForPreparedTokens(query, rawTokenStrs, rawTokens, linked, limit, opts);
1181
+ }
1182
+ function mergePhoneResults(results) {
1183
+ const byPrimaryId = new Map;
1184
+ for (const result of results) {
1185
+ const existing = byPrimaryId.get(result.primary._id);
1186
+ if (existing === undefined) {
1187
+ byPrimaryId.set(result.primary._id, result);
1188
+ continue;
1189
+ }
1190
+ const linkedById = new Map(existing.linked.map((item) => [item._id, item]));
1191
+ for (const item of result.linked)
1192
+ linkedById.set(item._id, item);
1193
+ existing.linked = Array.from(linkedById.values());
1194
+ existing.matchedIn = mergeMatchLocation(existing.matchedIn, result.matchedIn);
1195
+ }
1196
+ return Array.from(byPrimaryId.values());
1197
+ }
1198
+ function mergeMatchLocation(first, second) {
1199
+ if (first === "both" || second === "both" || first !== second)
1200
+ return "both";
1201
+ return first;
1202
+ }
1080
1203
  function findInLinkedArraysSimpleNumeric(query, linked, limit = 10, opts) {
1081
1204
  return findInLinkedArraysNumeric(query, linked, limit, opts).results;
1082
1205
  }
@@ -1590,6 +1713,15 @@ function firstNEqual3(a, b) {
1590
1713
  return false;
1591
1714
  return tokensEqual3(a, b);
1592
1715
  }
1716
+ function withCollectionPhoneNormalizers(opts, specs) {
1717
+ return {
1718
+ ...opts,
1719
+ phoneQueryNormalizers: [
1720
+ ...specs.map((spec) => resolvePhoneQueryNormalizer(spec)),
1721
+ ...opts.phoneQueryNormalizers ?? []
1722
+ ]
1723
+ };
1724
+ }
1593
1725
 
1594
1726
  class SearchUniverse {
1595
1727
  collections = new Map;
@@ -1950,7 +2082,11 @@ class SearchUniverse {
1950
2082
  primaryData: primaryCollection.data,
1951
2083
  linkedData: collection.data
1952
2084
  };
1953
- const linkedSearchResults = findInLinkedArraysNumeric(query, linkedMetadata, limit, searchOpts);
2085
+ const linkedSearchOpts = withCollectionPhoneNormalizers(searchOpts, [
2086
+ primaryCollection.config.spec,
2087
+ collection.config.spec
2088
+ ]);
2089
+ const linkedSearchResults = findInLinkedArraysNumeric(query, linkedMetadata, limit, linkedSearchOpts);
1954
2090
  if (linkedSearchResults.results.length > 0) {
1955
2091
  const combinedResults = [];
1956
2092
  const hasNegation = linkedSearchResults.hasNegation;
@@ -1973,7 +2109,7 @@ class SearchUniverse {
1973
2109
  if (inCollections && !inCollections.includes(name)) {
1974
2110
  continue;
1975
2111
  }
1976
- const matchingIds = searchInDataReturnIdsWithLimitNumeric(query, collection.metadata, limit, searchOpts);
2112
+ const matchingIds = searchInDataReturnIdsWithLimitNumeric(query, collection.metadata, limit, withCollectionPhoneNormalizers(searchOpts, [collection.config.spec]));
1977
2113
  if (matchingIds.length > 0) {
1978
2114
  const collectionResults = [];
1979
2115
  for (const id of matchingIds) {
@@ -2021,6 +2157,7 @@ export {
2021
2157
  scoreOne,
2022
2158
  scoreBag,
2023
2159
  sanitiseString,
2160
+ resolvePhoneQueryNormalizer,
2024
2161
  resetTokenRegistry,
2025
2162
  removePrimaryItemNumeric as removePrimaryItem,
2026
2163
  removeLinkedItemNumeric as removeLinkedItem,
@@ -2043,11 +2180,14 @@ export {
2043
2180
  findInLinkedArraysSimpleNumeric,
2044
2181
  findInLinkedArraysNumeric,
2045
2182
  encodeTokenWithMode,
2183
+ defaultQueryPhoneNormalizer,
2184
+ defaultPhoneNormalizer,
2046
2185
  decodeTokenId,
2047
2186
  decodeMatchMode,
2048
2187
  createTokensMap,
2049
2188
  createSearchLinkedMetadataNumeric as createSearchLinkedMetadata,
2050
2189
  createSearchArrayMetadataNumeric as createSearchArrayMetadata,
2190
+ compareTokenized,
2051
2191
  clearTokensMap,
2052
2192
  binarySearchUint32,
2053
2193
  arrayToSearchableData,
@@ -2059,5 +2199,6 @@ export {
2059
2199
  MATCH_MODE_PREFIXES,
2060
2200
  MATCH_MODE_BITS,
2061
2201
  DEFAULT_SEARCH_OPTS,
2202
+ DEFAULT_PHONE_FIELDS,
2062
2203
  AugmentedMetadata
2063
2204
  };
@@ -45,12 +45,18 @@ export interface SearchableObject {
45
45
  * - 'whole': exact match only - "rex" matches "rex" but not "rexona" or "ceporex"
46
46
  */
47
47
  export type MatchMode = 'start' | 'end' | 'startEnd' | 'anywhere' | 'whole';
48
+ /** Default root field names treated as phone numbers during metadata construction. */
49
+ export declare const DEFAULT_PHONE_FIELDS: readonly ["telefon", "phone", "gsm", "msisdn"];
50
+ /** Converts a stored phone value to canonical ASCII digits. */
51
+ export type PhoneNormalizer = (value: string) => string | undefined;
52
+ /** Recognizes a complete phone query and returns canonical ASCII digits. */
53
+ export type PhoneQueryNormalizer = (query: string) => string | undefined;
48
54
  /**
49
55
  * Specification for how to extract searchable text from objects
50
56
  */
51
57
  export interface SearchableObjectSpec<T extends SearchableObject, C = unknown> {
52
58
  /** Fields to include in search (if not specified, all string fields are used) */
53
- searchInFields?: (keyof T)[];
59
+ searchInFields?: readonly string[];
54
60
  /** Fields to exclude from search */
55
61
  dontSearchInFields?: (keyof T)[];
56
62
  /** Optional context passed to enrichSearchObjectFn */
@@ -65,6 +71,17 @@ export interface SearchableObjectSpec<T extends SearchableObject, C = unknown> {
65
71
  * Prefixes: < (start), > (end), + (startEnd), * (anywhere), ! (whole)
66
72
  */
67
73
  fieldMatchModes?: Partial<Record<keyof T, MatchMode>>;
74
+ /**
75
+ * Exact field paths whose values are normalized as phone numbers.
76
+ * @default DEFAULT_PHONE_FIELDS
77
+ */
78
+ phoneFields?: readonly string[];
79
+ /** Build normalizer for phone field values. @default defaultPhoneNormalizer */
80
+ phoneNormalizer?: PhoneNormalizer;
81
+ /** Query recognizer used by SearchUniverse for this collection. */
82
+ phoneQueryNormalizer?: PhoneQueryNormalizer;
83
+ /** Match mode for canonical phone tokens. @default 'anywhere' */
84
+ phoneMatchMode?: MatchMode;
68
85
  /**
69
86
  * Fields whose tokens are also stored in `metadata.unsorted` in SOURCE order
70
87
  * (no sort, no de-dup) for positional ranking. Fields are concatenated in
@@ -73,7 +90,7 @@ export interface SearchableObjectSpec<T extends SearchableObject, C = unknown> {
73
90
  * If omitted, `metadata.unsorted` has no entry for this item, and
74
91
  * positional ranking falls back to re-tokenizing primary text at query time.
75
92
  */
76
- keyFields?: (keyof T | string)[];
93
+ keyFields?: readonly string[];
77
94
  /**
78
95
  * Max number of tokens stored in `metadata.unsorted` per item.
79
96
  * Applied AFTER tokenization of concatenated `keyFields` content.
@@ -112,9 +129,14 @@ export interface SearchOpts {
112
129
  * @default 6
113
130
  */
114
131
  matchNumbersThreshold?: number;
132
+ /**
133
+ * Phone query recognizers evaluated before the ordinary tokenized query.
134
+ * When a phone variant finds candidates, it takes precedence over raw tokens.
135
+ */
136
+ phoneQueryNormalizers?: readonly PhoneQueryNormalizer[];
115
137
  }
116
- /** Default search options */
117
- export declare const DEFAULT_SEARCH_OPTS: Required<SearchOpts>;
138
+ /** Default match-mode options. Phone query normalizers are resolved per search. */
139
+ export declare const DEFAULT_SEARCH_OPTS: Required<Pick<SearchOpts, 'matchWords' | 'matchNumbers' | 'matchNumbersShort' | 'matchNumbersThreshold'>>;
118
140
  /**
119
141
  * Specification for linked search across two related arrays
120
142
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Compares two strings by their tokenized forms and returns the Jaccard
3
+ * similarity of their token sets: a number between `0` (no tokens in common)
4
+ * and `1` (identical token sets). Order-independent and case/diacritics
5
+ * insensitive, because both sides go through the same search pipeline as the
6
+ * rest of the library (normalizeDates → preprocess → sanitise → tokenize with
7
+ * the default replacements/removals).
8
+ *
9
+ * Both empty strings are considered identical (`1`); an empty vs non-empty
10
+ * string yields `0`.
11
+ *
12
+ * @param a - First string to compare.
13
+ * @param b - Second string to compare.
14
+ * @returns Similarity ratio `0..1` (matching tokens / union of tokens).
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * compareTokenized('Janez Novak', 'novak janez'); // 1
19
+ * compareTokenized('Janes Novak', 'novak'); // 0.5
20
+ * compareTokenized('Janez Novak', 'ana'); // 0
21
+ * ```
22
+ */
23
+ export declare function compareTokenized(a: string, b: string): number;
24
+ //# sourceMappingURL=compareTokenized.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compareTokenized.d.ts","sourceRoot":"","sources":["../../src/utils/compareTokenized.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAc7D"}
@@ -0,0 +1,26 @@
1
+ import type { PhoneNormalizer, PhoneQueryNormalizer, SearchableObject, SearchableObjectSpec } from '../types';
2
+ /**
3
+ * Converts a stored phone value to its canonical ASCII digit representation.
4
+ *
5
+ * @feature [phone-search]
6
+ */
7
+ export declare const defaultPhoneNormalizer: PhoneNormalizer;
8
+ /**
9
+ * Recognizes a complete phone query before applying default canonicalization.
10
+ *
11
+ * @feature [phone-search]
12
+ */
13
+ export declare const defaultQueryPhoneNormalizer: PhoneQueryNormalizer;
14
+ /**
15
+ * Verifies that a custom normalizer returned the canonical phone representation.
16
+ *
17
+ * @feature [phone-search]
18
+ */
19
+ export declare function validateNormalizedPhone(normalized: string | undefined, normalizerName: string): string | undefined;
20
+ /**
21
+ * Resolves the query normalizer that converges with a collection's build normalizer.
22
+ *
23
+ * @feature [phone-search]
24
+ */
25
+ export declare function resolvePhoneQueryNormalizer<T extends SearchableObject>(spec?: SearchableObjectSpec<T>): PhoneQueryNormalizer;
26
+ //# sourceMappingURL=phoneNormalization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phoneNormalization.d.ts","sourceRoot":"","sources":["../../src/utils/phoneNormalization.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,UAAU,CAAC;AAMlB;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,eAGpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,oBAGzC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,cAAc,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAMpB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,gBAAgB,EACpE,IAAI,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC7B,oBAAoB,CAQtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-search",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "A fast, memory-efficient search library for large datasets with support for tokenized matching, linked collections, and per-field match modes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",