indickit 0.5.0 → 0.6.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/README.md CHANGED
@@ -3,12 +3,14 @@
3
3
  Small, exact utilities for text in the languages of India, in Go and
4
4
  TypeScript. They fix the places where ordinary string code goes wrong on
5
5
  Indian text: a search that misses a word because it is spelled, encoded or
6
- inflected another way, and a letter count that cuts a letter in two.
6
+ inflected another way, a letter count that cuts a letter in two, and a name
7
+ that must be written in Latin letters.
7
8
 
8
9
  ```
9
10
  stem किताब किताबें किताबों → किताब
10
11
  phonetic राम ರಾಮ രാമ ராம رام Ram → rn
11
12
  segment ಲಕ್ಷ್ಮಿ → ಲ ಕ್ಷ್ಮಿ
13
+ romanize लक्ष्मी ലക്ഷ്മി லக்ஷ்மி → lakshmi
12
14
  ```
13
15
 
14
16
  Each utility gives the same output in Go and in TypeScript, and each was
@@ -23,6 +25,7 @@ measured on data that its rules were never built on.
23
25
  | let a search for हिन्दी also find हिंदी, and keep words whole | `fold` |
24
26
  | find a person's name typed in another script | `phonetic` |
25
27
  | count letters, cut at a length, or move a cursor | `segment` |
28
+ | write a name or a text in Latin letters (लक्ष्मी → lakshmi) | `romanize` |
26
29
 
27
30
  ## Install
28
31
 
@@ -47,13 +50,20 @@ TypeScript, set `moduleResolution` to `nodenext` or `bundler`; the old
47
50
  In a browser, with no build step:
48
51
 
49
52
  ```js
50
- import { normalize } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.5.0/dist/normalize.js";
51
- import { stem } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.5.0/dist/stem.js";
52
- import { match } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.5.0/dist/phonetic.js";
53
- import { segment } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.5.0/dist/segment.js";
53
+ import { normalize } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.6.0/dist/normalize.js";
54
+ import { stem } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.6.0/dist/stem.js";
55
+ import { match } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.6.0/dist/phonetic.js";
56
+ import { segment } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.6.0/dist/segment.js";
57
+ import { load } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.6.0/dist/romanize.js";
54
58
  ```
55
59
 
56
60
  Each utility is its own file, so a page loads only the rules it uses.
61
+ `romanize` also loads one language's tables when you ask for them (0.2–1.8
62
+ MB). The npm package carries no tables: `load()` reads them beside the
63
+ module (a jsDelivr or GitHub-tag install), else from jsDelivr at the same
64
+ version. Offline, pass your own `fetch`. In Go, `go get` downloads the
65
+ tables of every language once (14 MB); a program carries only the languages
66
+ it imports.
57
67
 
58
68
  ## Recipes
59
69
 
@@ -152,6 +162,33 @@ segment("ਕ੍ਕ"); // ["ਕ੍","ਕ"]: Gurmukhi shows this virama, so these
152
162
  count("ಕನ್ನಡ"); // 3
153
163
  ```
154
164
 
165
+ ### Write it in Latin letters
166
+
167
+ `romanize` gives a ranked list of spellings, most likely first. Use the
168
+ `"words"` mode for text and the `"names"` mode for a field that holds a
169
+ person's name:
170
+
171
+ ```ts
172
+ import { load } from "indickit/romanize";
173
+
174
+ const hi = await load("hi"); // fetches the Hindi tables
175
+ hi.word("लक्ष्मी", 2); // ["lakshmi","laxmi"]
176
+ hi.text("भारत के प्रधानमंत्री"); // "bharat ke pradhaanmantri"
177
+ const ur = await load("ur", "names");
178
+ ur.word("پرویز", 1); // ["parvez"]
179
+ ```
180
+
181
+ In Go, import the languages you need; a program carries only their tables:
182
+
183
+ ```go
184
+ import (
185
+ "github.com/micahchoo/indickit/romanize"
186
+ _ "github.com/micahchoo/indickit/romanize/lang/hi"
187
+ )
188
+
189
+ romanize.Word("लक्ष्मी", "hi", romanize.Words, 2) // [lakshmi laxmi]
190
+ ```
191
+
155
192
  ### The same in Go
156
193
 
157
194
  ```go
@@ -182,6 +219,9 @@ segment.Segment("ಲಕ್ಷ್ಮಿ") // [ಲ ಕ್ಷ
182
219
  | `phonetic.Words(name)` | `words(name)` | the words, split as the others split them |
183
220
  | `segment.Segment(s)` | `segment(text)` | the letters, in order; joined, they give the text back |
184
221
  | `segment.Count(s)` | `count(text)` | how many letters a reader sees |
222
+ | `romanize.Word(w, lang, mode, n)` | `(await load(lang, mode)).word(w, n?)` | up to n spellings, most likely first |
223
+ | `romanize.Text(s, lang, mode)` | `(await load(lang, mode)).text(text)` | each word of the language's script in Latin letters |
224
+ | `romanize.Languages(mode)` | `languages(mode)` | the languages with tables (Go: those imported) |
185
225
 
186
226
  Three things hold for all of them:
187
227
 
@@ -208,6 +248,7 @@ other tools tried and each weak spot are in `docs/`.
208
248
  | `stem` | finds 6–15 points more of the right sentences than exact search, in 13 languages | ahead in 5 languages, level in 2, a trade in 4, behind in Nepali | 3 KB | [docs/stem.md](docs/stem.md) |
209
249
  | `phonetic` | finds the right person 84% of the time, in 21 languages, on clean names; 42% on a real roster as written (70% with titles and initials removed) | romanize + Soundex: 23% in Tamil, 70% in Hindi; cannot read Urdu | 4 KB | [docs/phonetic.md](docs/phonetic.md) |
210
250
  | `segment` | cuts 0.05% of Kannada words wrongly | `Intl.Segmenter`: 50.3% | 9 KB | [docs/segment.md](docs/segment.md) |
251
+ | `romanize` | writes 48.9% of the words of running text as people typed them, in 11 languages; 71.3% of names (tables: 0.2–1.8 MB a language) | IndicXlit (Python, a 119 MB model): 37.9% and 49.0% | 5 KB | [docs/romanize.md](docs/romanize.md) |
211
252
 
212
253
  (Browser files are gzipped.)
213
254
 
@@ -231,6 +272,9 @@ other tools tried and each weak spot are in `docs/`.
231
272
  - **Some languages have no table.** `stem` covers 13 languages; Bodo,
232
273
  Dogri, Kashmiri, Konkani, Maithili, Manipuri, Odia, Santali and Sindhi
233
274
  are not among them.
275
+ - **`romanize` is behind IndicXlit on places and on rare single words**
276
+ (by 7–13 and 5.5 points). Santali has names only; Bodo and Dogri have
277
+ text only. See [docs/romanize.md](docs/romanize.md).
234
278
  - **Go reads invalid UTF-8 as U+FFFD**, so for such input the output does
235
279
  not join back into the original bytes.
236
280
  - **"The same output" holds inside the blocks indickit reads.** Go reads
@@ -249,10 +293,12 @@ Each utility's own weak spots are at the end of its file in `docs/`.
249
293
 
250
294
  Every table and switch of a utility is in its rules file
251
295
  (`normalize/rules.json`, `stem/rules.json`, `phonetic/rules.json`,
252
- `segment/rules.json`); the Go and TypeScript code is a short loop over it.
296
+ `segment/rules.json`; `romanize/rules.json` and its tables in
297
+ `romanize/lang/`); the Go and TypeScript code is a short loop over it.
253
298
  Both are checked against a conformance file of inputs with the outputs that
254
299
  a reference implementation gave them: 345,276 inputs for `normalize`,
255
- 418,665 for `stem`, 195,994 for `phonetic`, 858,654 for `segment`
300
+ 418,665 for `stem`, 195,994 for `phonetic`, 858,654 for `segment`, 2,397
301
+ for `romanize`
256
302
  (`*/testdata/conformance.jsonl.gz`). A change that makes any one disagree
257
303
  on any input fails the build.
258
304
 
@@ -0,0 +1,743 @@
1
+ // romanize/rules.json
2
+ var rules_default = {
3
+ beam: {
4
+ names: 12,
5
+ words: 8
6
+ },
7
+ bos: "<s>",
8
+ eos: "</s>",
9
+ families: {
10
+ names: {
11
+ as: {
12
+ file: "as.names.bin",
13
+ group: "brahmic",
14
+ lookup: 669,
15
+ pooled: "names-brahmic.bin",
16
+ tag: "as"
17
+ },
18
+ bn: {
19
+ file: "bn.names.bin",
20
+ group: "brahmic",
21
+ lookup: 1785,
22
+ pooled: "names-brahmic.bin",
23
+ tag: "bn"
24
+ },
25
+ gom: {
26
+ file: "gom.names.bin",
27
+ group: "brahmic",
28
+ lookup: 17,
29
+ pooled: "names-brahmic.bin",
30
+ tag: "gom"
31
+ },
32
+ gu: {
33
+ file: "gu.names.bin",
34
+ group: "brahmic",
35
+ lookup: 131,
36
+ pooled: "names-brahmic.bin",
37
+ tag: "gu"
38
+ },
39
+ hi: {
40
+ file: "hi.names.bin",
41
+ group: "brahmic",
42
+ lookup: 1152,
43
+ pooled: "names-brahmic.bin",
44
+ tag: "hi"
45
+ },
46
+ kn: {
47
+ file: "kn.names.bin",
48
+ group: "brahmic",
49
+ lookup: 448,
50
+ pooled: "names-brahmic.bin",
51
+ tag: "kn"
52
+ },
53
+ ks: {
54
+ file: "ks.names.bin",
55
+ group: "arabic",
56
+ lookup: 130,
57
+ pooled: "names-arabic.bin",
58
+ tag: "ks"
59
+ },
60
+ mai: {
61
+ file: "mai.names.bin",
62
+ group: "brahmic",
63
+ lookup: 150,
64
+ pooled: "names-brahmic.bin",
65
+ tag: "mai"
66
+ },
67
+ ml: {
68
+ file: "ml.names.bin",
69
+ group: "brahmic",
70
+ lookup: 908,
71
+ pooled: "names-brahmic.bin",
72
+ tag: "ml"
73
+ },
74
+ mni: {
75
+ file: "mni.names.bin",
76
+ group: "meetei",
77
+ lookup: 253,
78
+ pooled: "names-meetei.bin",
79
+ tag: "mni"
80
+ },
81
+ mr: {
82
+ file: "mr.names.bin",
83
+ group: "brahmic",
84
+ lookup: 716,
85
+ pooled: "names-brahmic.bin",
86
+ tag: "mr"
87
+ },
88
+ ne: {
89
+ file: "ne.names.bin",
90
+ group: "brahmic",
91
+ lookup: 146,
92
+ pooled: "names-brahmic.bin",
93
+ tag: "ne"
94
+ },
95
+ or: {
96
+ file: "or.names.bin",
97
+ group: "brahmic",
98
+ lookup: 610,
99
+ pooled: "names-brahmic.bin",
100
+ tag: "or"
101
+ },
102
+ pa: {
103
+ file: "pa.names.bin",
104
+ group: "brahmic",
105
+ lookup: 996,
106
+ pooled: "names-brahmic.bin",
107
+ tag: "pa"
108
+ },
109
+ sa: {
110
+ file: "sa.names.bin",
111
+ group: "brahmic",
112
+ lookup: 66,
113
+ pooled: "names-brahmic.bin",
114
+ tag: "sa"
115
+ },
116
+ sat: {
117
+ file: "sat.names.bin",
118
+ group: "olchiki",
119
+ lookup: 407,
120
+ pooled: "names-olchiki.bin",
121
+ tag: "sat"
122
+ },
123
+ sd: {
124
+ file: "sd.names.bin",
125
+ group: "arabic",
126
+ lookup: 72,
127
+ pooled: "names-arabic.bin",
128
+ tag: "sd"
129
+ },
130
+ ta: {
131
+ file: "ta.names.bin",
132
+ group: "brahmic",
133
+ lookup: 3961,
134
+ pooled: "names-brahmic.bin",
135
+ tag: "ta"
136
+ },
137
+ te: {
138
+ file: "te.names.bin",
139
+ group: "brahmic",
140
+ lookup: 2126,
141
+ pooled: "names-brahmic.bin",
142
+ tag: "te"
143
+ },
144
+ ur: {
145
+ file: "ur.names.bin",
146
+ group: "arabic",
147
+ lookup: 1961,
148
+ pooled: "names-arabic.bin",
149
+ tag: "ur"
150
+ }
151
+ },
152
+ words: {
153
+ as: {
154
+ file: "as.words.bin",
155
+ group: "brahmic",
156
+ lookup: 0,
157
+ pooled: "words-brahmic.bin",
158
+ tag: "as:w"
159
+ },
160
+ bn: {
161
+ file: "bn.words.bin",
162
+ group: "brahmic",
163
+ lookup: 0,
164
+ pooled: "words-brahmic.bin",
165
+ tag: "bn:w"
166
+ },
167
+ brx: {
168
+ file: "brx.words.bin",
169
+ group: "brahmic",
170
+ lookup: 0,
171
+ pooled: "words-brahmic.bin",
172
+ tag: "brx:w"
173
+ },
174
+ doi: {
175
+ file: "doi.words.bin",
176
+ group: "brahmic",
177
+ lookup: 0,
178
+ pooled: "words-brahmic.bin",
179
+ tag: "doi:w"
180
+ },
181
+ gom: {
182
+ file: "gom.words.bin",
183
+ group: "brahmic",
184
+ lookup: 0,
185
+ pooled: "words-brahmic.bin",
186
+ tag: "gom:w"
187
+ },
188
+ gu: {
189
+ file: "gu.words.bin",
190
+ group: "brahmic",
191
+ lookup: 0,
192
+ pooled: "words-brahmic.bin",
193
+ tag: "gu:w"
194
+ },
195
+ hi: {
196
+ file: "hi.words.bin",
197
+ group: "brahmic",
198
+ lookup: 0,
199
+ pooled: "words-brahmic.bin",
200
+ tag: "hi:w"
201
+ },
202
+ kn: {
203
+ file: "kn.words.bin",
204
+ group: "brahmic",
205
+ lookup: 0,
206
+ pooled: "words-brahmic.bin",
207
+ tag: "kn:w"
208
+ },
209
+ ks: {
210
+ file: "ks.words.bin",
211
+ group: "arabic",
212
+ lookup: 0,
213
+ pooled: "words-arabic.bin",
214
+ tag: "ks:w"
215
+ },
216
+ mai: {
217
+ file: "mai.words.bin",
218
+ group: "brahmic",
219
+ lookup: 0,
220
+ pooled: "words-brahmic.bin",
221
+ tag: "mai:w"
222
+ },
223
+ ml: {
224
+ file: "ml.words.bin",
225
+ group: "brahmic",
226
+ lookup: 0,
227
+ pooled: "words-brahmic.bin",
228
+ tag: "ml:w"
229
+ },
230
+ mni: {
231
+ file: "mni.words.bin",
232
+ group: "meetei",
233
+ lookup: 0,
234
+ pooled: "words-meetei.bin",
235
+ tag: "mni:w"
236
+ },
237
+ mr: {
238
+ file: "mr.words.bin",
239
+ group: "brahmic",
240
+ lookup: 0,
241
+ pooled: "words-brahmic.bin",
242
+ tag: "mr:w"
243
+ },
244
+ ne: {
245
+ file: "ne.words.bin",
246
+ group: "brahmic",
247
+ lookup: 0,
248
+ pooled: "words-brahmic.bin",
249
+ tag: "ne:w"
250
+ },
251
+ or: {
252
+ file: "or.words.bin",
253
+ group: "brahmic",
254
+ lookup: 0,
255
+ pooled: "words-brahmic.bin",
256
+ tag: "or:w"
257
+ },
258
+ pa: {
259
+ file: "pa.words.bin",
260
+ group: "brahmic",
261
+ lookup: 0,
262
+ pooled: "words-brahmic.bin",
263
+ tag: "pa:w"
264
+ },
265
+ sa: {
266
+ file: "sa.words.bin",
267
+ group: "brahmic",
268
+ lookup: 0,
269
+ pooled: "words-brahmic.bin",
270
+ tag: "sa:w"
271
+ },
272
+ sd: {
273
+ file: "sd.words.bin",
274
+ group: "arabic",
275
+ lookup: 0,
276
+ pooled: "words-arabic.bin",
277
+ tag: "sd:w"
278
+ },
279
+ ta: {
280
+ file: "ta.words.bin",
281
+ group: "brahmic",
282
+ lookup: 0,
283
+ pooled: "words-brahmic.bin",
284
+ tag: "ta:w"
285
+ },
286
+ te: {
287
+ file: "te.words.bin",
288
+ group: "brahmic",
289
+ lookup: 0,
290
+ pooled: "words-brahmic.bin",
291
+ tag: "te:w"
292
+ },
293
+ ur: {
294
+ file: "ur.words.bin",
295
+ group: "arabic",
296
+ lookup: 0,
297
+ pooled: "words-arabic.bin",
298
+ tag: "ur:w"
299
+ }
300
+ }
301
+ },
302
+ files: {
303
+ "as.names.bin": 89974,
304
+ "as.words.bin": 235757,
305
+ "bn.names.bin": 182011,
306
+ "bn.words.bin": 377491,
307
+ "brx.words.bin": 208654,
308
+ "doi.words.bin": 48681,
309
+ "gom.names.bin": 9361,
310
+ "gom.words.bin": 324293,
311
+ "gu.names.bin": 38634,
312
+ "gu.words.bin": 370117,
313
+ "hi.names.bin": 162471,
314
+ "hi.words.bin": 468425,
315
+ "kn.names.bin": 73095,
316
+ "kn.words.bin": 273776,
317
+ "ks.names.bin": 36223,
318
+ "ks.words.bin": 445883,
319
+ "mai.names.bin": 40421,
320
+ "mai.words.bin": 381362,
321
+ "ml.names.bin": 133130,
322
+ "ml.words.bin": 342656,
323
+ "mni.names.bin": 12382,
324
+ "mni.words.bin": 133350,
325
+ "mr.names.bin": 119082,
326
+ "mr.words.bin": 327753,
327
+ "names-arabic.bin": 188775,
328
+ "names-brahmic.bin": 594055,
329
+ "names-meetei.bin": 43805,
330
+ "names-olchiki.bin": 97591,
331
+ "ne.names.bin": 37468,
332
+ "ne.words.bin": 297072,
333
+ "or.names.bin": 83867,
334
+ "or.words.bin": 285972,
335
+ "pa.names.bin": 140045,
336
+ "pa.words.bin": 419426,
337
+ "sa.names.bin": 24600,
338
+ "sa.words.bin": 243993,
339
+ "sat.names.bin": 73397,
340
+ "sd.names.bin": 11896,
341
+ "sd.words.bin": 463475,
342
+ "ta.names.bin": 295030,
343
+ "ta.words.bin": 244532,
344
+ "te.names.bin": 229077,
345
+ "te.words.bin": 295013,
346
+ "ur.names.bin": 192400,
347
+ "ur.words.bin": 540255,
348
+ "words-arabic.bin": 1182486,
349
+ "words-brahmic.bin": 2542643,
350
+ "words-meetei.bin": 198229,
351
+ "words-olchiki.bin": 61705
352
+ },
353
+ groups: {
354
+ arabic: [
355
+ 1536,
356
+ 1791
357
+ ],
358
+ brahmic: [
359
+ 2304,
360
+ 3455
361
+ ],
362
+ meetei: [
363
+ 43744,
364
+ 44031
365
+ ],
366
+ olchiki: [
367
+ 7248,
368
+ 7295
369
+ ]
370
+ },
371
+ max_n: 3,
372
+ order: 3,
373
+ rules_version: "2026-10-07",
374
+ unify: {
375
+ from: [
376
+ 2432,
377
+ 3455
378
+ ],
379
+ mask: 127,
380
+ to_block: 2304
381
+ },
382
+ unknown_penalty: 20,
383
+ version: 1
384
+ };
385
+ // package.json
386
+ var version = "0.6.0";
387
+
388
+ // js/lang.ts
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 ";
390
+ function langCode(tag) {
391
+ const t = (tag ?? "").toLowerCase().split(/[-_]/)[0];
392
+ const i = t.length === 3 ? ISO_639_2.indexOf(t + " ") : -1;
393
+ return i >= 0 && i % 7 === 0 ? ISO_639_2.slice(i + 4, i + 6) : t;
394
+ }
395
+
396
+ // js/romanize.ts
397
+ var RULES = rules_default;
398
+ var RULES_VERSION = RULES.rules_version;
399
+ function languages(mode = "words") {
400
+ return Object.keys(RULES.families[mode]).sort();
401
+ }
402
+ var CDN = `https://cdn.jsdelivr.net/gh/micahchoo/indickit@v${version}/romanize/lang/`;
403
+ async function fetchBytes(url) {
404
+ const res = await fetch(url);
405
+ if (!res.ok)
406
+ throw new Error(`romanize: ${url}: ${res.status}`);
407
+ return res.arrayBuffer();
408
+ }
409
+ function defaultFetcher(local) {
410
+ return async (url) => {
411
+ try {
412
+ return await fetchBytes(url);
413
+ } catch {
414
+ return fetchBytes(new URL(url.href.slice(local.href.length), CDN));
415
+ }
416
+ };
417
+ }
418
+ function code(lang) {
419
+ const c = langCode(lang);
420
+ return c === "kok" ? "gom" : c;
421
+ }
422
+ var pooledCache = new Map;
423
+ async function load(lang, mode = "words", opts = {}) {
424
+ const c = code(lang);
425
+ const l = RULES.families[mode][c];
426
+ if (!l)
427
+ throw new Error(`romanize: no ${mode} tables for "${lang}"`);
428
+ const base = new URL(opts.base ?? "../romanize/lang/", import.meta.url);
429
+ const get = opts.fetch ?? (opts.base ? fetchBytes : defaultFetcher(base));
430
+ const poolKey = `${base}${l.group}/${l.pooled}`;
431
+ if (!pooledCache.has(poolKey))
432
+ pooledCache.set(poolKey, get(new URL(`${l.group}/${l.pooled}`, base)));
433
+ const [own, pool] = await Promise.all([get(new URL(`${c}/${l.file}`, base)), pooledCache.get(poolKey)]);
434
+ return fromBytes(c, mode, new Uint8Array(own), new Uint8Array(pool));
435
+ }
436
+ function fromBytes(lang, mode, own, pool) {
437
+ const l = RULES.families[mode][lang];
438
+ if (!l)
439
+ throw new Error(`romanize: no ${mode} tables for "${lang}"`);
440
+ const x = new Mix(own, pool, l.tag, RULES.beam[mode]);
441
+ const [lo, hi] = RULES.groups[l.group];
442
+ const inScript = (ch) => {
443
+ const cp = ch.codePointAt(0);
444
+ return cp >= lo && cp <= hi || cp === 8204 || cp === 8205;
445
+ };
446
+ const letter = /[\p{L}\p{M}‌‍]/u;
447
+ const word = (w, n = 4) => {
448
+ const s = w.normalize("NFC");
449
+ const known = x.lookup.get(s);
450
+ if (known)
451
+ return known.slice(0, n);
452
+ return x.decode(unify(s), n).filter((o) => o !== "");
453
+ };
454
+ const r = {
455
+ lang,
456
+ mode,
457
+ word,
458
+ text(t) {
459
+ const cs = [...t.normalize("NFC")];
460
+ let out = "";
461
+ for (let i = 0;i < cs.length; ) {
462
+ if (!(inScript(cs[i]) && letter.test(cs[i]))) {
463
+ out += cs[i++];
464
+ continue;
465
+ }
466
+ let j = i;
467
+ while (j < cs.length && inScript(cs[j]) && letter.test(cs[j]))
468
+ j++;
469
+ const run = cs.slice(i, j).join("");
470
+ out += word(run, 1)[0] ?? run;
471
+ i = j;
472
+ }
473
+ return out;
474
+ }
475
+ };
476
+ Object.defineProperty(r, "_mix", { value: x });
477
+ return r;
478
+ }
479
+ function unify(w) {
480
+ const u = RULES.unify;
481
+ let out = "";
482
+ for (const ch of w) {
483
+ const cp = ch.codePointAt(0);
484
+ out += cp >= u.from[0] && cp <= u.from[1] ? String.fromCodePoint(u.to_block + (cp & u.mask)) : ch;
485
+ }
486
+ return out;
487
+ }
488
+
489
+ class Reader {
490
+ b;
491
+ pos = 4;
492
+ constructor(b) {
493
+ this.b = b;
494
+ if (b[0] !== 73 || b[1] !== 75 || b[2] !== 82 || b[3] !== 49)
495
+ throw new Error("romanize: not an IKR1 file");
496
+ }
497
+ varint() {
498
+ let v = 0;
499
+ let shift = 1;
500
+ for (;; ) {
501
+ const byte = this.b[this.pos++];
502
+ v += (byte & 127) * shift;
503
+ if (byte < 128)
504
+ return v;
505
+ shift *= 128;
506
+ }
507
+ }
508
+ str() {
509
+ const n = this.varint();
510
+ const s = DECODER.decode(this.b.subarray(this.pos, this.pos + n));
511
+ this.pos += n;
512
+ return s;
513
+ }
514
+ float64() {
515
+ const v = new DataView(this.b.buffer, this.b.byteOffset + this.pos, 8).getFloat64(0, true);
516
+ this.pos += 8;
517
+ return v;
518
+ }
519
+ }
520
+ var DECODER = new TextDecoder;
521
+ var ID = 2 ** 21;
522
+ function ctxKey(ctx, from) {
523
+ let k = 0;
524
+ for (let i = from;i < ctx.length; i++)
525
+ k = k * ID + (ctx[i] + 1);
526
+ return k;
527
+ }
528
+
529
+ class Model {
530
+ vocab;
531
+ levels = [];
532
+ constructor(r, strs, ids) {
533
+ this.vocab = r.varint();
534
+ for (let l = r.varint();l > 0; l--) {
535
+ const lv = { stats: new Map, counts: new Map };
536
+ for (let c = r.varint();c > 0; c--) {
537
+ const ctx = [];
538
+ for (let i = r.varint();i > 0; i--)
539
+ ctx.push(ids.id(strs[r.varint()]));
540
+ const key = ctxKey(ctx, 0);
541
+ const counts = new Map;
542
+ let total = 0;
543
+ let prev = 0;
544
+ for (let t = r.varint();t > 0; t--) {
545
+ prev += r.varint();
546
+ const k = r.varint();
547
+ counts.set(ids.id(strs[prev]), k);
548
+ total += k;
549
+ }
550
+ lv.stats.set(key, [total, counts.size]);
551
+ lv.counts.set(key, counts);
552
+ }
553
+ this.levels.push(lv);
554
+ }
555
+ }
556
+ stats(ctx) {
557
+ const s = { counts: [], total: [], wb: [] };
558
+ for (let n = 0;n < this.levels.length; n++) {
559
+ const key = ctxKey(ctx, ctx.length - n);
560
+ const st = this.levels[n].stats.get(key);
561
+ if (st && st[1] !== 0) {
562
+ s.counts[n] = this.levels[n].counts.get(key);
563
+ s.total[n] = st[0];
564
+ s.wb[n] = st[0] / (st[0] + st[1]);
565
+ }
566
+ }
567
+ return s;
568
+ }
569
+ prob(s, tok) {
570
+ let p = 1 / (this.vocab + 1);
571
+ for (let n = 0;n < this.levels.length; n++) {
572
+ const c = s.counts[n];
573
+ if (!c)
574
+ continue;
575
+ const lam = s.wb[n];
576
+ p = lam * (c.get(tok) ?? 0) / s.total[n] + (1 - lam) * p;
577
+ }
578
+ return p;
579
+ }
580
+ }
581
+
582
+ class Interner {
583
+ ids = new Map;
584
+ strs = [];
585
+ id(s) {
586
+ let v = this.ids.get(s);
587
+ if (v === undefined) {
588
+ v = this.strs.length;
589
+ this.ids.set(s, v);
590
+ this.strs.push(s);
591
+ }
592
+ return v;
593
+ }
594
+ }
595
+ function before(a, b) {
596
+ if (a.score !== b.score)
597
+ return a.score > b.score;
598
+ if (a.out !== b.out)
599
+ return a.out < b.out;
600
+ return a.idx < b.idx;
601
+ }
602
+ function top(cs, k) {
603
+ const best = [];
604
+ for (const c of cs) {
605
+ if (best.length === k && !before(c, best[k - 1]))
606
+ continue;
607
+ let lo = 0;
608
+ let hi = best.length;
609
+ while (lo < hi) {
610
+ const mid = lo + hi >> 1;
611
+ if (before(c, best[mid]))
612
+ hi = mid;
613
+ else
614
+ lo = mid + 1;
615
+ }
616
+ best.splice(lo, 0, c);
617
+ if (best.length > k)
618
+ best.pop();
619
+ }
620
+ return best;
621
+ }
622
+
623
+ class Mix {
624
+ beam;
625
+ a;
626
+ b;
627
+ lam;
628
+ start = [];
629
+ eos;
630
+ options = new Map;
631
+ lookup = new Map;
632
+ constructor(own, pool, tag, beam) {
633
+ this.beam = beam;
634
+ const ids = new Interner;
635
+ let r = new Reader(pool);
636
+ let strs = readStrings(r);
637
+ this.b = new Model(r, strs, ids);
638
+ r = new Reader(own);
639
+ strs = readStrings(r);
640
+ this.lam = r.float64();
641
+ this.a = new Model(r, strs, ids);
642
+ for (let k = r.varint();k > 0; k--) {
643
+ const native = r.str();
644
+ const sp = [];
645
+ for (let i = r.varint();i > 0; i--)
646
+ sp.push(strs[r.varint()]);
647
+ this.lookup.set(native, sp);
648
+ }
649
+ for (let i = 0;i < RULES.order - 2; i++)
650
+ this.start.push(ids.id(RULES.bos));
651
+ this.start.push(ids.id(`<s:${tag}>`));
652
+ this.eos = ids.id(RULES.eos);
653
+ const seen = new Set;
654
+ for (const m of [this.a, this.b]) {
655
+ for (const counts of m.levels[0].counts.values()) {
656
+ for (const t of counts.keys()) {
657
+ if (t === this.eos || seen.has(t))
658
+ continue;
659
+ seen.add(t);
660
+ const s = ids.strs[t];
661
+ const i = s.indexOf("|");
662
+ const chunk = s.slice(0, i);
663
+ const list = this.options.get(chunk) ?? [];
664
+ list.push({ tok: t, en: s.slice(i + 1) });
665
+ this.options.set(chunk, list);
666
+ }
667
+ }
668
+ }
669
+ for (const list of this.options.values())
670
+ list.sort((p, q) => p.en < q.en ? -1 : p.en > q.en ? 1 : 0);
671
+ if (this.start.length > 2 || ids.strs.length >= ID)
672
+ throw new Error("romanize: beyond the engine's limits");
673
+ }
674
+ logp(sa, sb, tok) {
675
+ const pa = Math.exp(Math.log(this.a.prob(sa, tok)));
676
+ const pb = Math.exp(Math.log(this.b.prob(sb, tok)));
677
+ return Math.log(this.lam * pa + (1 - this.lam) * pb);
678
+ }
679
+ decode(word, n) {
680
+ const w = [...word];
681
+ const h = RULES.order - 1;
682
+ const root = { score: 0, out: "", ctx: this.start };
683
+ const beams = Array.from({ length: w.length + 1 }, () => []);
684
+ beams[0].push({ score: 0, out: "", parent: null, tok: -1, idx: 0 });
685
+ const context = (c) => {
686
+ if (c.tok < 0)
687
+ return c.parent.ctx;
688
+ const ctx = [...c.parent.ctx, c.tok];
689
+ return ctx.slice(ctx.length - h);
690
+ };
691
+ for (let i = 0;i < w.length; i++) {
692
+ if (beams[i].length === 0)
693
+ continue;
694
+ for (const c of top(beams[i], this.beam)) {
695
+ const hy = i === 0 ? root : { score: c.score, out: c.out, ctx: context(c) };
696
+ let moved = false;
697
+ const sa = this.a.stats(hy.ctx);
698
+ const sb = this.b.stats(hy.ctx);
699
+ for (let k = 1;k <= RULES.max_n && i + k <= w.length; k++) {
700
+ const opts = this.options.get(w.slice(i, i + k).join(""));
701
+ if (!opts)
702
+ continue;
703
+ for (const o of opts) {
704
+ const next = beams[i + k];
705
+ next.push({ score: hy.score + this.logp(sa, sb, o.tok), out: hy.out + o.en, parent: hy, tok: o.tok, idx: next.length });
706
+ moved = true;
707
+ }
708
+ }
709
+ if (!moved) {
710
+ const next = beams[i + 1];
711
+ next.push({ score: hy.score - RULES.unknown_penalty, out: hy.out, parent: hy, tok: -1, idx: next.length });
712
+ }
713
+ }
714
+ }
715
+ const final = new Map;
716
+ for (const c of beams[w.length]) {
717
+ const ctx = c.parent === null ? this.start : context(c);
718
+ const s = c.score + this.logp(this.a.stats(ctx), this.b.stats(ctx), this.eos);
719
+ const v = final.get(c.out);
720
+ if (v === undefined || s > v)
721
+ final.set(c.out, s);
722
+ }
723
+ return [...final].sort((p, q) => p[1] !== q[1] ? q[1] - p[1] : p[0] < q[0] ? -1 : p[0] > q[0] ? 1 : 0).slice(0, n).map((e) => e[0]);
724
+ }
725
+ }
726
+ function readStrings(r) {
727
+ const strs = [];
728
+ for (let i = r.varint();i > 0; i--)
729
+ strs.push(r.str());
730
+ return strs;
731
+ }
732
+ function _decode(r, word, n) {
733
+ return r._mix.decode(word, n);
734
+ }
735
+ export {
736
+ unify,
737
+ load,
738
+ languages,
739
+ fromBytes,
740
+ _decode,
741
+ RULES_VERSION,
742
+ CDN
743
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Indian-language text in Latin letters, the way people spell it:
3
+ * लक्ष्मी → lakshmi, മലൈക → malaika, پرویز → parvez.
4
+ *
5
+ * A ranked list: one word has several accepted spellings (Choudhury,
6
+ * Chowdhury), 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, where a lookup of known names comes first.
9
+ *
10
+ * The tables are learned from data and loaded per language, on demand:
11
+ *
12
+ * const r = await load("hi"); // fetches the Hindi and Brahmic files (beside this module, else jsDelivr)
13
+ * r.word("लक्ष्मी"); // ["lakshmi", ...]
14
+ *
15
+ * The output is deterministic: the same input gives the same list in Go and
16
+ * in TypeScript.
17
+ *
18
+ * @module
19
+ */
20
+ /** The tables: "words" for running text, "names" for person names. */
21
+ export type Mode = "words" | "names";
22
+ /** Changes whenever any output changes: store it beside romanized text. */
23
+ export declare const RULES_VERSION: string;
24
+ /** The language codes that have tables, for one mode. */
25
+ export declare function languages(mode?: Mode): string[];
26
+ /** Fetches one data file's bytes. The default reads it beside this module. */
27
+ export type Fetcher = (url: URL) => Promise<ArrayBuffer>;
28
+ /** Where the tables are when they are not beside this module: jsDelivr, at this package's version. */
29
+ export declare const CDN: string;
30
+ /** One language's loaded tables. */
31
+ export interface Romanizer {
32
+ readonly lang: string;
33
+ readonly mode: Mode;
34
+ /** Up to n spellings of one word (default 4), most likely first. */
35
+ word(word: string, n?: number): string[];
36
+ /** Every run of the language's script romanized (its top spelling); everything else kept. */
37
+ text(text: string): string;
38
+ }
39
+ /**
40
+ * Loads a language's tables for one mode: its own file and its script
41
+ * group's pooled file (shared by the group, fetched once). It reads them
42
+ * beside this module, else from jsDelivr at this package's version (the npm
43
+ * package carries no tables); `base` or `fetch` replaces both. Rejects when
44
+ * the language has no tables for the mode.
45
+ */
46
+ export declare function load(lang: string, mode?: Mode, opts?: {
47
+ fetch?: Fetcher;
48
+ base?: URL | string;
49
+ }): Promise<Romanizer>;
50
+ /** Builds a Romanizer from a language's two files (for tests and offline use). */
51
+ export declare function fromBytes(lang: string, mode: Mode, own: Uint8Array, pool: Uint8Array): Romanizer;
52
+ /** Moves every Brahmic code point to the Devanagari block by its offset (rules.json "unify"). */
53
+ export declare function unify(w: string): string;
54
+ /** @internal The model's own list for a unified word (no lookup), for the conformance test. */
55
+ export declare function _decode(r: Romanizer, word: string, n: number): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indickit",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -25,6 +25,10 @@
25
25
  "./stem": {
26
26
  "types": "./dist/types/js/stem.d.ts",
27
27
  "default": "./dist/stem.js"
28
+ },
29
+ "./romanize": {
30
+ "types": "./dist/types/js/romanize.d.ts",
31
+ "default": "./dist/romanize.js"
28
32
  }
29
33
  },
30
34
  "files": [
@@ -32,7 +36,7 @@
32
36
  ],
33
37
  "sideEffects": false,
34
38
  "scripts": {
35
- "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 && tsc -p tsconfig.json",
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",
36
40
  "test": "bun test",
37
41
  "check-dist": "node js/check-dist.mjs"
38
42
  },