indickit 0.6.0 → 0.8.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/dist/phonetic.js CHANGED
@@ -1,11 +1,14 @@
1
1
  // phonetic/rules.json
2
2
  var rules_default = {
3
- version: "2026-10-06.1",
3
+ version: "2026-10-07",
4
4
  folds: [
5
5
  "bn-case",
6
+ "brahmic-av-u",
6
7
  "drop-vowels",
7
8
  "drop-y",
9
+ "flap-r",
8
10
  "gu-case",
11
+ "latin-ngh",
9
12
  "latin-w-u",
10
13
  "ml-case",
11
14
  "nasal-all",
@@ -100,7 +103,13 @@ var rules_default = {
100
103
  },
101
104
  virama_offset: 77,
102
105
  candrabindu_offset: 1,
103
- candrabindu_class: "ṁ"
106
+ candrabindu_class: "ṁ",
107
+ nukta_offset: 60,
108
+ flap_offsets: [
109
+ 33,
110
+ 34
111
+ ],
112
+ flap_class: "ṛ"
104
113
  },
105
114
  extra: {
106
115
  ৎ: "t",
@@ -117,7 +126,14 @@ var rules_default = {
117
126
  ൿ: "k",
118
127
  ൔ: "m",
119
128
  ൕ: "y",
120
- ൖ: "l"
129
+ ൖ: "l",
130
+ ड़: "ṛ",
131
+ ढ़: "ṛ",
132
+ ড়: "ṛ",
133
+ ঢ়: "ṛ",
134
+ ଡ଼: "ṛ",
135
+ ଢ଼: "ṛ",
136
+ ੜ: "ṛ"
121
137
  },
122
138
  urdu: {
123
139
  from: 1536,
@@ -139,7 +155,7 @@ var rules_default = {
139
155
  ڈ: "ṭ",
140
156
  ذ: "c",
141
157
  ر: "r",
142
- ڑ: "ṭ",
158
+ ڑ: "ṛ",
143
159
  ز: "c",
144
160
  ژ: "c",
145
161
  س: "s",
@@ -181,7 +197,7 @@ var rules_default = {
181
197
  ڌ: "t",
182
198
  ڍ: "ṭ",
183
199
  ڏ: "t",
184
- ڙ: "ṭ",
200
+ ڙ: "ṛ",
185
201
  ڦ: "p",
186
202
  ڪ: "k",
187
203
  ڱ: "ṅ",
@@ -301,7 +317,7 @@ var rules_default = {
301
317
  ᱯ: "p",
302
318
  ᱰ: "ṭ",
303
319
  ᱱ: "n",
304
- ᱲ: "ṭ",
320
+ ᱲ: "ṛ",
305
321
  ᱳ: "o",
306
322
  ᱴ: "ṭ",
307
323
  ᱵ: "p",
@@ -313,6 +329,10 @@ var rules_default = {
313
329
  },
314
330
  latin: {
315
331
  groups: [
332
+ [
333
+ "ngh",
334
+ "nĝ"
335
+ ],
316
336
  [
317
337
  "chh",
318
338
  "c"
@@ -420,9 +440,11 @@ var rules_default = {
420
440
  v: "p",
421
441
  w: "p",
422
442
  ñ: "n",
443
+ ĝ: "k",
423
444
  ṁ: "n",
424
445
  ṅ: "n",
425
446
  ṇ: "n",
447
+ ṛ: "t",
426
448
  ṭ: "t"
427
449
  },
428
450
  vowels: "aeiou",
@@ -436,6 +458,26 @@ var rules_default = {
436
458
  "drop-final-vowel": false
437
459
  },
438
460
  branches: [
461
+ {
462
+ from: 2304,
463
+ to: 3455,
464
+ class: "v",
465
+ also: "u",
466
+ after: "aṁ",
467
+ not_before: "aieo"
468
+ },
469
+ {
470
+ from: 1536,
471
+ to: 7295,
472
+ class: "ṛ",
473
+ also: "r"
474
+ },
475
+ {
476
+ from: 0,
477
+ to: 127,
478
+ class: "ĝ",
479
+ also: "h"
480
+ },
439
481
  {
440
482
  from: 0,
441
483
  to: 127,
@@ -455,6 +497,10 @@ var rules_default = {
455
497
  also: "u"
456
498
  }
457
499
  ],
500
+ joined: {
501
+ min_classes: 8,
502
+ when: "the strict keys find nobody"
503
+ },
458
504
  digit_zeros: [
459
505
  48,
460
506
  1632,
@@ -565,6 +611,7 @@ var rules_default = {
565
611
  };
566
612
 
567
613
  // js/phonetic.ts
614
+ var DELETED = "\x00";
568
615
  function compile(rules) {
569
616
  const b = rules.brahmic;
570
617
  const byOffset = new Array(128);
@@ -588,26 +635,31 @@ function compile(rules) {
588
635
  }
589
636
  function readBrahmic(word) {
590
637
  let out = "";
638
+ let last = -1;
591
639
  for (let i = 0;i < word.length; i++) {
592
640
  const ch = word[i];
593
641
  const x = extra.get(ch);
594
642
  if (x !== undefined) {
595
643
  out += x;
644
+ last = -1;
596
645
  continue;
597
646
  }
598
647
  const o = word.charCodeAt(i);
599
648
  if (o < b.from || o > b.to)
600
649
  continue;
601
650
  const off = o & 127;
602
- if (off === b.virama_offset)
603
- continue;
604
- if (off === b.candrabindu_offset) {
651
+ const c = byOffset[off];
652
+ if (off === b.nukta_offset && b.flap_offsets.includes(last) && out) {
653
+ out = out.slice(0, -1) + b.flap_class;
654
+ } else if (off === b.virama_offset) {} else if (off === b.candrabindu_offset) {
605
655
  out += b.candrabindu_class;
606
- continue;
656
+ } else if (c !== undefined) {
657
+ out += c;
607
658
  }
608
- const c = byOffset[off];
609
659
  if (c !== undefined)
610
- out += c;
660
+ last = off;
661
+ else if (off !== b.nukta_offset && off !== b.virama_offset)
662
+ last = -1;
611
663
  }
612
664
  return out;
613
665
  }
@@ -728,6 +780,11 @@ function compile(rules) {
728
780
  runs += out[i];
729
781
  return runs;
730
782
  }
783
+ function fits(classes, i, br) {
784
+ const prev = i > 0 ? classes[i - 1] : "^";
785
+ const next = i + 1 < classes.length ? classes[i + 1] : "$";
786
+ return (br.after === undefined || br.after.includes(prev)) && (br.before === undefined || br.before.includes(next)) && !(br.not_before ?? "").includes(next);
787
+ }
731
788
  function variants(word) {
732
789
  const classes = read(word);
733
790
  let vs = [classes];
@@ -745,12 +802,13 @@ function compile(rules) {
745
802
  if (!inScript)
746
803
  continue;
747
804
  for (let i = 0;i < classes.length; i++) {
748
- if (classes[i] === br.class && vs.length * 2 <= rules.max_keys) {
749
- vs = vs.concat(vs.map((v) => v.slice(0, i) + br.also + v.slice(i + 1)));
805
+ if (classes[i] === br.class && vs.length * 2 <= rules.max_keys && fits(classes, i, br)) {
806
+ const also = br.also || DELETED;
807
+ vs = vs.concat(vs.map((v) => v.slice(0, i) + also + v.slice(i + 1)));
750
808
  }
751
809
  }
752
810
  }
753
- return vs;
811
+ return vs.map((v) => v.replaceAll(DELETED, ""));
754
812
  }
755
813
  function number(word) {
756
814
  let out = "";
@@ -788,6 +846,7 @@ function compile(rules) {
788
846
  };
789
847
  }
790
848
  var engine = compile(rules_default);
849
+ var _internal = { compile: (r) => compile(r), normalize: (w) => normalize(w), engine };
791
850
  var RULES_VERSION = rules_default.version;
792
851
  var MAX_NAME_KEYS = 256;
793
852
  function normalize(word) {
@@ -817,6 +876,13 @@ function nameKeys(name) {
817
876
  }
818
877
  return out[0] === "" ? [] : out;
819
878
  }
879
+ var JOINED_MIN_CLASSES = rules_default.joined.min_classes;
880
+ function joinedKeys(name) {
881
+ const joined = words(name).map(normalize).join("");
882
+ if (!joined)
883
+ return [];
884
+ return engine(joined).filter((k) => [...k].length >= JOINED_MIN_CLASSES);
885
+ }
820
886
  function match(a, b) {
821
887
  const wa = words(a), wb = words(b);
822
888
  if (!wa.length || wa.length !== wb.length)
@@ -831,6 +897,9 @@ export {
831
897
  nameKeys,
832
898
  match,
833
899
  keys,
900
+ joinedKeys,
901
+ _internal,
834
902
  RULES_VERSION,
835
- MAX_NAME_KEYS
903
+ MAX_NAME_KEYS,
904
+ JOINED_MIN_CLASSES
836
905
  };
package/dist/romanize.js CHANGED
@@ -383,7 +383,7 @@ var rules_default = {
383
383
  version: 1
384
384
  };
385
385
  // package.json
386
- var version = "0.6.0";
386
+ var version = "0.8.0";
387
387
 
388
388
  // js/lang.ts
389
389
  var ISO_639_2 = "asm as ben bn guj gu hin hi kan kn kas ks mal ml mar mr nep ne ori or pan pa san sa snd sd tam ta tel te urd ur ";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Indian-language text from Latin typing, the reverse of romanize:
3
+ * namaste → नमस्ते, ahmad → احمد, vanakkam → வணக்கம்.
4
+ *
5
+ * A ranked list: one typed word can stand for several native words (kamal:
6
+ * कमल, कमाल), and the first is the most likely. Two modes: "words" for
7
+ * running text (the default) and "names" for fields known to hold person
8
+ * names.
9
+ *
10
+ * It writes the word it is given in the language's script, so English words
11
+ * in mixed text ("kal meeting hai") are written in it too: identify each
12
+ * word's language first.
13
+ *
14
+ * The tables are learned from data and loaded per language, on demand:
15
+ *
16
+ * const d = await load("hi"); // fetches the Hindi and Brahmic files (beside this module, else jsDelivr)
17
+ * d.word("namaste"); // ["नमस्ते", ...]
18
+ *
19
+ * The output is deterministic: the same input gives the same list in Go and
20
+ * in TypeScript.
21
+ *
22
+ * @module
23
+ */
24
+ /** The tables: "words" for running text, "names" for person names. */
25
+ export type Mode = "words" | "names";
26
+ /** Changes whenever any output changes. */
27
+ export declare const RULES_VERSION: string;
28
+ /** The language codes that have tables, for one mode. */
29
+ export declare function languages(mode?: Mode): string[];
30
+ /** Fetches one data file's bytes. The default reads it beside this module. */
31
+ export type Fetcher = (url: URL) => Promise<ArrayBuffer>;
32
+ /** Where the tables are when they are not beside this module: jsDelivr, at this package's version. */
33
+ export declare const CDN: string;
34
+ /** One language's loaded tables. */
35
+ export interface Deromanizer {
36
+ readonly lang: string;
37
+ readonly mode: Mode;
38
+ /** Up to n native spellings of one Latin-typed word (default 4), most likely first. */
39
+ word(latin: string, n?: number): string[];
40
+ /** Every run of Latin letters written as its first native spelling; everything else kept. */
41
+ text(text: string): string;
42
+ }
43
+ /** The input as the training pairs were read: accents removed, lower case, a-z only. */
44
+ export declare function clean(s: string): string;
45
+ /**
46
+ * Loads a language's tables for one mode: its own files, its word list and its
47
+ * script group's pooled files (shared by the group, fetched once). It reads them
48
+ * beside this module, else from jsDelivr at this package's version (the npm
49
+ * package carries no tables); `base` or `fetch` replaces both. Rejects when the
50
+ * language has no tables for the mode.
51
+ */
52
+ export declare function load(lang: string, mode?: Mode, opts?: {
53
+ fetch?: Fetcher;
54
+ base?: URL | string;
55
+ }): Promise<Deromanizer>;
56
+ /** Builds a Deromanizer from its files, by file name (for tests and offline use). */
57
+ export declare function fromBytes(lang: string, mode: Mode, files: Record<string, Uint8Array>): Deromanizer;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Phonetic search: the key finds candidates, and a scorer learned from data
3
+ * ranks them 0..100. A Latin query finds a name in any script; a name in one
4
+ * Indian script finds it in another.
5
+ *
6
+ * const s = await loadSearch("hi"); // fetches the Hindi table (a few KB)
7
+ * const ix = s.index(["नरेंद्र मोदी", "नरेश मोदी"]);
8
+ * ix.search("Narendra Modi", THRESHOLDS.names); // [{ name: 0, score: 100 }]
9
+ *
10
+ * Two profiles: "names" (a list of names: a roster, a directory) and "text"
11
+ * (a name among the words of a running text). A score is relative to the
12
+ * other candidates of its search, so it belongs to the search, not to a pair.
13
+ * Every step is integer arithmetic: Go, TypeScript and the reference give the
14
+ * same scores.
15
+ *
16
+ * @module
17
+ */
18
+ /** "names": a list of names; "text": a name among a text's words. */
19
+ export type Profile = "names" | "text";
20
+ type ProfileRules = {
21
+ key: number;
22
+ cost: number;
23
+ vowel: number;
24
+ div: number;
25
+ stop_permille: number | null;
26
+ stop_min_names: number;
27
+ round: boolean;
28
+ };
29
+ /** Changes whenever any score changes. */
30
+ export declare const SEARCH_VERSION: string;
31
+ /** The scores measured for each use: names in a list 74, villages 72, a name
32
+ * in text 80 (at most 10% of hits wrong) or 70 (20%). */
33
+ export declare const THRESHOLDS: Readonly<Record<string, number>>;
34
+ type Sparse = [number, [number, number][]];
35
+ type RawTable = {
36
+ latin?: string[];
37
+ native?: string[];
38
+ x?: string[];
39
+ y?: string[];
40
+ c: Sparse[];
41
+ units?: [number, number, number, [number, number][]][];
42
+ };
43
+ declare class CostTable {
44
+ readonly idx: Map<string, number>;
45
+ readonly v: number;
46
+ readonly c: Float64Array;
47
+ readonly units: Map<number, Float64Array<ArrayBufferLike>>;
48
+ constructor(raw: RawTable);
49
+ ids(rs: string[]): number[];
50
+ align(a: number[], b: number[], withUnits: boolean): number;
51
+ }
52
+ /** Fetches one data file's bytes. The default reads it beside this module, else from jsDelivr. */
53
+ export type Fetcher = (url: URL) => Promise<ArrayBuffer>;
54
+ /** Where the tables are when they are not beside this module: jsDelivr, at this package's version. */
55
+ export declare const CDN: string;
56
+ declare class Scorer {
57
+ readonly lang: string;
58
+ private latin;
59
+ private pairs;
60
+ constructor(lang: string, latin: CostTable | undefined, pairs: Map<string, CostTable>);
61
+ cost(a: string, b: string): number | undefined;
62
+ calibrations(a: string, cands: string[]): Map<string, number>;
63
+ wordSim(a: string, b: string, cal: Map<string, number> | undefined, p: ProfileRules): number;
64
+ }
65
+ /** A name found by a search: its position among the indexed names, and its score. */
66
+ export interface Hit {
67
+ name: number;
68
+ score: number;
69
+ }
70
+ /** Names of one language, keyed once for searching. */
71
+ export declare class Index {
72
+ private readonly scorer;
73
+ private readonly words;
74
+ private readonly n;
75
+ private readonly df;
76
+ private readonly maps;
77
+ private readonly p;
78
+ /** @internal Use Search.index. */
79
+ constructor(scorer: Scorer, names: string[], profile: Profile);
80
+ private common;
81
+ private weight;
82
+ private candidates;
83
+ private nameScore;
84
+ /** @internal */
85
+ scoreAll(query: string, which: number[]): Hit[];
86
+ /** The names scoring at least threshold, best first (ties in index order). */
87
+ search(query: string, threshold?: number): Hit[];
88
+ }
89
+ /** A loaded language: index names, or score candidates. */
90
+ export interface Search {
91
+ readonly lang: string;
92
+ /** Key names (of this language, or Latin) once for searching. */
93
+ index(names: string[], profile?: Profile): Index;
94
+ /** One score per candidate name, calibrated among these candidates. */
95
+ score(query: string, candidates: string[], profile?: Profile): number[];
96
+ }
97
+ /** Loads one language's table (Latin queries against its script) and, for
98
+ * native-to-native search, the tables between the scripts named in `scripts`
99
+ * (e.g. ["deva", "arab", "beng"]). A missing table falls back to the fine keys. */
100
+ export declare function loadSearch(lang: string, options?: {
101
+ scripts?: string[];
102
+ fetcher?: Fetcher;
103
+ }): Promise<Search>;
104
+ export {};
@@ -10,6 +10,13 @@
10
10
  *
11
11
  * @module
12
12
  */
13
+ /** @internal For ./phonetic-search: the engine compiler, the normalizer and the
14
+ * search key's engine (keys of a normalized word). Not part of the API. */
15
+ export declare const _internal: {
16
+ compile: (r: unknown) => (word: string) => string[];
17
+ normalize: (w: string) => string;
18
+ engine: (word: string) => string[];
19
+ };
13
20
  /** The rules every key from this module was made with. */
14
21
  export declare const RULES_VERSION: string;
15
22
  /** NameKeys stops here: a long Tamil name can have many key combinations. */
@@ -23,6 +30,13 @@ export declare function words(name: string): string[];
23
30
  * separated by a space. Two names match when these share an element, so
24
31
  * these are what to index. At most MAX_NAME_KEYS. */
25
32
  export declare function nameKeys(name: string): string[];
33
+ /** Joined keys shorter than this (in classes) are not returned: they find too many names. */
34
+ export declare const JOINED_MIN_CLASSES: number;
35
+ /** The keys of a name written as one word, its words joined: "Ram Nath" and
36
+ * இராம்நாத் share no nameKeys, but can share a joined key. Only keys of at
37
+ * least JOINED_MIN_CLASSES classes. A search looks them up only when
38
+ * nameKeys find nobody; match does not use them. */
39
+ export declare function joinedKeys(name: string): string[];
26
40
  /** Whether two names have the same number of words and every pair of words,
27
41
  * in order, shares a key. */
28
42
  export declare function match(a: string, b: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indickit",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Small, exact utilities for text in Indian languages: a phonetic key that matches a name across 21 Indian scripts and English, a normalizer that gives text that looks the same one encoding, a segmenter for the letters a reader sees, and a stemmer for search.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.14",
@@ -29,6 +29,14 @@
29
29
  "./romanize": {
30
30
  "types": "./dist/types/js/romanize.d.ts",
31
31
  "default": "./dist/romanize.js"
32
+ },
33
+ "./phonetic-search": {
34
+ "types": "./dist/types/js/phonetic-search.d.ts",
35
+ "default": "./dist/phonetic-search.js"
36
+ },
37
+ "./deromanize": {
38
+ "types": "./dist/types/js/deromanize.d.ts",
39
+ "default": "./dist/deromanize.js"
32
40
  }
33
41
  },
34
42
  "files": [
@@ -36,7 +44,7 @@
36
44
  ],
37
45
  "sideEffects": false,
38
46
  "scripts": {
39
- "build": "bun build js/phonetic.ts --outfile dist/phonetic.js --target browser --format esm && bun build js/normalize.ts --outfile dist/normalize.js --target browser --format esm && bun build js/segment.ts --outfile dist/segment.js --target browser --format esm && bun build js/stem.ts --outfile dist/stem.js --target browser --format esm && bun build js/romanize.ts --outfile dist/romanize.js --target browser --format esm && tsc -p tsconfig.json",
47
+ "build": "bun build js/phonetic.ts --outfile dist/phonetic.js --target browser --format esm && bun build js/normalize.ts --outfile dist/normalize.js --target browser --format esm && bun build js/segment.ts --outfile dist/segment.js --target browser --format esm && bun build js/stem.ts --outfile dist/stem.js --target browser --format esm && bun build js/romanize.ts --outfile dist/romanize.js --target browser --format esm && bun build js/phonetic-search.ts --outfile dist/phonetic-search.js --target browser --format esm && bun build js/deromanize.ts --outfile dist/deromanize.js --target browser --format esm && tsc -p tsconfig.json",
40
48
  "test": "bun test",
41
49
  "check-dist": "node js/check-dist.mjs"
42
50
  },