indickit 0.8.0 → 0.10.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
@@ -7,12 +7,16 @@ inflected another way, a letter count that cuts a letter in two, a name
7
7
  that must be written in Latin letters, and Latin typing that must be written
8
8
  in an Indian script.
9
9
 
10
+ Try each utility in your browser, in all 22 languages:
11
+ https://micahchoo.github.io/indickit/docs/demo/
12
+
10
13
  ```
11
14
  stem किताब किताबें किताबों → किताब
12
15
  phonetic राम ರಾಮ രാമ ராம رام Ram → rn
13
16
  segment ಲಕ್ಷ್ಮಿ → ಲ ಕ್ಷ್ಮಿ
14
17
  romanize लक्ष्मी ലക്ഷ്മി லக்ஷ்மி → lakshmi
15
18
  deromanize namaste → नमस्ते
19
+ wellformed िहन्दी हिन्दी → broken whole
16
20
  ```
17
21
 
18
22
  Each utility gives the same output in Go and in TypeScript, and each was
@@ -29,7 +33,9 @@ measured on data that its rules were never built on.
29
33
  | find a name in a long list, as people write it (titles, initials), or in running text | `phonetic-search` |
30
34
  | count letters, cut at a length, or move a cursor | `segment` |
31
35
  | write a name or a text in Latin letters (लक्ष्मी → lakshmi) | `romanize` |
36
+ | let a search for "modi" find मोदी | `romanize` the index; see the recipe |
32
37
  | write Latin typing in an Indian script (namaste → नमस्ते) | `deromanize` |
38
+ | reject or flag text that no font can draw whole (a PDF text layer) | `wellformed` |
33
39
 
34
40
  ## Languages
35
41
 
@@ -50,7 +56,7 @@ others hold tables for some languages only.
50
56
  | Konkani | `gom` | Devanagari | – | ✓ | words, names | words, names |
51
57
  | Maithili | `mai` | Devanagari | – | ✓ | words, names | words, names |
52
58
  | Malayalam | `ml` | Malayalam | ✓ | ✓ | words, names | words, names |
53
- | Manipuri | `mni` | Meetei Mayek | – | ✓ | words, names | words, names |
59
+ | Manipuri | `mni` | Meetei Mayek, Bengali-Assamese | – | ✓ | words, names | words, names |
54
60
  | Marathi | `mr` | Devanagari | ✓ | ✓ | words, names | words, names |
55
61
  | Nepali | `ne` | Devanagari | ✓ | ✓ | words, names | words, names |
56
62
  | Odia | `or` | Odia | – | ✓ | words, names | words, names |
@@ -91,13 +97,14 @@ TypeScript, set `moduleResolution` to `nodenext` or `bundler`; the old
91
97
  In a browser, with no build step:
92
98
 
93
99
  ```js
94
- import { normalize } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.8.0/dist/normalize.js";
95
- import { stem } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.8.0/dist/stem.js";
96
- import { match } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.8.0/dist/phonetic.js";
97
- import { segment } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.8.0/dist/segment.js";
98
- import { load } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.8.0/dist/romanize.js";
99
- import { loadSearch } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.8.0/dist/phonetic-search.js";
100
- import { load as loadDeromanize } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.8.0/dist/deromanize.js";
100
+ import { normalize } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/normalize.js";
101
+ import { stem } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/stem.js";
102
+ import { match } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/phonetic.js";
103
+ import { segment } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/segment.js";
104
+ import { load } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/romanize.js";
105
+ import { loadSearch } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/phonetic-search.js";
106
+ import { load as loadDeromanize } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/deromanize.js";
107
+ import { isWellFormed } from "https://cdn.jsdelivr.net/gh/micahchoo/indickit@v0.10.0/dist/wellformed.js";
101
108
  ```
102
109
 
103
110
  Each utility is its own file, so a page loads only the rules it uses.
@@ -126,6 +133,22 @@ normalize("അവന്\u200d", "ml"); // "അവൻ": the old chillu becomes th
126
133
  normalize("र्\u200dय", "mr"); // "र्\u200dय": eyelash ra; this ZWJ is visible, so it stays
127
134
  ```
128
135
 
136
+ ### Reject text no font can draw
137
+
138
+ A PDF text layer or an OCR run gives words that no font can draw whole: a
139
+ vowel sign typed before its letter, a conjunct with a member dropped. A
140
+ renderer shows a dotted circle (◌) there. `normalize` cannot repair such a
141
+ word, because it never moves a sign. `wellformed` finds it before you store
142
+ it: 10.8% of the words in PDF text layers are broken, 0.012% in clean text.
143
+
144
+ ```ts
145
+ import { isWellFormed, brokenAt } from "indickit/wellformed";
146
+
147
+ isWellFormed("\u093fहन्दी"); // false: the vowel sign ि has no letter before it
148
+ isWellFormed("हिन्दी"); // true
149
+ brokenAt("क्ि"); // 2: a vowel sign after a virama
150
+ ```
151
+
129
152
  ### Search words in their other forms
130
153
 
131
154
  Give every word of a document a key, and give the query the same key. The
@@ -161,6 +184,12 @@ fold("हिन्दी", "hi"); // "हिंदी"
161
184
  fold("गाँव", "hi"); // "गांव"
162
185
  ```
163
186
 
187
+ Urdu and Sindhi often write two words as one (ہو گیا and ہوگیا). `fold`
188
+ does not join them: joining the stored text glues 39% of Urdu words to the
189
+ word before. Instead, search a phrase twice, as typed and with its spaces
190
+ removed. On new Wikipedia text that finds 3–4% more of a phrase's hits in
191
+ Urdu and 6–14% in Sindhi, with 0.6% and 1.3% wrong hits.
192
+
164
193
  ### Find a name in any script
165
194
 
166
195
  One person's name is typed in Devanagari on one form, in Malayalam on a
@@ -253,6 +282,42 @@ import (
253
282
  romanize.Word("लक्ष्मी", "hi", romanize.Words, 2) // [lakshmi laxmi]
254
283
  ```
255
284
 
285
+ ### Find native text with a Latin query
286
+
287
+ A user types "modi" in a search box; the store holds मोदी. When you build
288
+ the index, romanize each stored word in the `"words"` mode and store its
289
+ four most likely spellings in a Latin field beside it:
290
+
291
+ ```ts
292
+ import { load } from "indickit/romanize";
293
+
294
+ const hi = await load("hi"); // the "words" mode
295
+ hi.word("मोदी", 4); // ["modi","modee","moudi","mody"]
296
+ hi.word("दिल्ली", 4); // ["dilli","dili","dilly","dilley"]
297
+ hi.word("सरकार", 4); // ["sarkar","sarkaar","sarakar","sarakaar"]
298
+ ```
299
+
300
+ At query time, lower-case the query and fuzzy-match it against that field:
301
+ a hit is a word with a stored spelling one or two edits from the query
302
+ (Elasticsearch: `fuzziness: AUTO`). The browser needs no model, and the
303
+ index needs no download beyond the tables it was romanized with.
304
+
305
+ On Wikipedia sentences typed in Latin letters by native speakers (Dakshina,
306
+ 11 languages, held out), the first hit is the right word 81.4% of the time,
307
+ and one of the first four is 91.3%. IndicXlit, a 119 MB neural model that
308
+ writes the query in the native script, gives 81.8% and 87.6%. The index
309
+ leads in Bengali, Hindi, Kannada, Malayalam, Marathi, Tamil and Telugu, is
310
+ level in Gujarati and Punjabi, and trails in Sindhi and Urdu. Its cost is a
311
+ long list: 24 hits a search, and more than half of the first ten are other
312
+ words. See [docs/romanize.md](docs/romanize.md#a-latin-query-on-a-romanized-index).
313
+
314
+ Two other ways were measured. Do not put the `phonetic` key on both sides:
315
+ it finds the right word first 38.0% of the time and floods the list; it is
316
+ for names, not words. Do not write the query in the native script
317
+ (`deromanize`, the next recipe) for a ranked list: that is the right way
318
+ for a strict filter, where every hit must be the word itself, but for a
319
+ ranked list the romanized index is as good and needs no second download.
320
+
256
321
  ### Write Latin typing in an Indian script
257
322
 
258
323
  `deromanize` is the reverse: it gives a ranked list of native spellings for
@@ -289,9 +354,11 @@ import (
289
354
  "github.com/micahchoo/indickit/phonetic"
290
355
  "github.com/micahchoo/indickit/segment"
291
356
  "github.com/micahchoo/indickit/stem"
357
+ "github.com/micahchoo/indickit/wellformed"
292
358
  )
293
359
 
294
360
  normalize.Text("അവന്\u200d", "ml") // "അവൻ"
361
+ wellformed.Check("\u093fहन्दी") // false
295
362
  stem.Stem(normalize.Text("किताबों", "hi"), "hi") // किताब
296
363
  phonetic.Match("Mohanlal", "മോഹൻലാൽ") // true
297
364
  phonetic.NewIndex(names, "hi", phonetic.ProfileNames).Search("Narendra Modi", phonetic.Thresholds["names"])
@@ -322,6 +389,8 @@ segment.Segment("ಲಕ್ಷ್ಮಿ") // [ಲ ಕ್ಷ
322
389
  | `deromanize.Word(latin, lang, mode, n)` | `(await load(lang, mode)).word(latin, n?)` | up to n native spellings, most likely first |
323
390
  | `deromanize.Text(s, lang, mode)` | `(await load(lang, mode)).text(text)` | each run of Latin letters in the language's script |
324
391
  | `deromanize.Languages(mode)` | `languages(mode)` | the languages with tables (Go: those imported) |
392
+ | `wellformed.Check(s)` | `isWellFormed(text)` | true when a font draws the text with no dotted circle |
393
+ | `wellformed.BrokenAt(s)` | `brokenAt(text)` | where the first dotted circle would be (Go: byte index; TypeScript: UTF-16 index), or -1 |
325
394
 
326
395
  Three things hold for all of them:
327
396
 
@@ -344,12 +413,13 @@ other tools tried and each weak spot are in `docs/`.
344
413
 
345
414
  | Utility | Result | Best other tool | Browser file | Details |
346
415
  |---|---|---|---|---|
347
- | `normalize` | makes 88.1% of look-alike spellings equal, and changes the look of no word | Indic NLP Library: 82.4%, and changes 0.68% of words | 14 KB | [docs/normalize.md](docs/normalize.md) |
416
+ | `normalize` | makes 88.1% of look-alike spellings equal, and changes the look of no word | Indic NLP Library: 82.4%, and changes 0.68% of words | 16 KB | [docs/normalize.md](docs/normalize.md) |
348
417
  | `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) |
349
- | `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); with `phonetic-search`, 86% of a real roster as written and 85% of villages | romanize + Soundex: 23% in Tamil, 70% in Hindi; cannot read Urdu; ICU + fuzzy match: 78% and 65% | 5 KB | [docs/phonetic.md](docs/phonetic.md) |
350
- | `segment` | cuts 0.05% of Kannada words wrongly | `Intl.Segmenter`: 50.3% | 9 KB | [docs/segment.md](docs/segment.md) |
351
- | `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) |
352
- | `deromanize` | writes the right word first for 87.1% of the words of Hindi news typed in Latin, and in four for 97.4%; 69.7% of names in 19 languages (tables: 0.3–3.1 MB a language) | IndicXlit: 86.1%, 89.7% and 66.4%; behind on rare words out of context | 20 KB | [docs/deromanize.md](docs/deromanize.md) |
418
+ | `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); with `phonetic-search`, 86% of a real roster as written and 85% of villages | romanize + Soundex: 23% in Tamil, 70% in Hindi; cannot read Urdu; ICU + fuzzy match: 78% and 65% | 7 KB | [docs/phonetic.md](docs/phonetic.md) |
419
+ | `segment` | cuts 0.05% of Kannada words wrongly | `Intl.Segmenter`: 50.3% | 8 KB | [docs/segment.md](docs/segment.md) |
420
+ | `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% | 7 KB | [docs/romanize.md](docs/romanize.md) |
421
+ | `deromanize` | writes the right word first for 87.1% of the words of Hindi news typed in Latin, and in four for 97.4%; 69.7% of names in 19 languages (tables: 0.3–3.1 MB a language) | IndicXlit: 86.1%, 89.7% and 66.4%; behind on rare words out of context | 21 KB | [docs/deromanize.md](docs/deromanize.md) |
422
+ | `wellformed` | agrees with HarfBuzz on 100% of 135,944 held-out PDF words (9,594 of them broken) and 2,146,974 new Wikipedia words | a check that the word starts with a combining sign: finds 84% of the broken words, with 4,667 false alarms | 6 KB | [docs/wellformed.md](docs/wellformed.md) |
353
423
 
354
424
  (Browser files are gzipped.)
355
425
 
@@ -385,14 +455,38 @@ best of them, indic-trans (about 200 MB), gives 34.7% on running text against
385
455
  are not among them.
386
456
  - **`romanize` is behind IndicXlit on places and on rare single words**
387
457
  (by 7–13 and 5.5 points). Santali has names only; Bodo and Dogri have
388
- text only. See [docs/romanize.md](docs/romanize.md).
458
+ text only. Manipuri in Bengali script loses syllables in the words mode;
459
+ its tables were trained on Meetei Mayek. See [docs/romanize.md](docs/romanize.md).
460
+ - **`normalize` keeps the direction marks LRM and RLM** (U+200E, U+200F).
461
+ Text copied from the web carries them, and they hide a match on 0.04% of
462
+ Wikipedia redirect titles. Next to right-to-left text or digits, deleting
463
+ one can change the order a reader sees, and the glyph oracle cannot see
464
+ that. Strip them yourself before `normalize` when the text has no
465
+ right-to-left part.
466
+ - **`wellformed` gives HarfBuzz's verdict only.** DirectWrite (Windows) and
467
+ CoreText (Apple) insert dotted circles by their own grammars. Arabic and
468
+ Ol Chiki text is always whole. A verdict that needs more than three
469
+ characters of context (three or more invisible characters in a row) can
470
+ be wrong. See [docs/wellformed.md](docs/wellformed.md).
471
+ - **Cap user input before `phonetic-search`.** Its cost grows with (query
472
+ words) x (candidate words): a 4,096-character query takes seconds. See
473
+ [docs/phonetic.md](docs/phonetic.md#search-phonetic-search).
474
+ - **A word of more than 256 letters gets one spelling** from `romanize`
475
+ and `deromanize`: each 256-letter piece is spelled alone and the pieces
476
+ are joined. A run that long is not a word; the cap keeps the time linear.
389
477
  - **Go reads invalid UTF-8 as U+FFFD**, so for such input the output does
390
478
  not join back into the original bytes.
391
- - **"The same output" holds inside the blocks indickit reads.** Go reads
392
- Unicode 15.0; TypeScript reads the Unicode data of its host (node 24:
393
- 17.0). A newer mark outside those blocks can give another output: "Ram"
394
- plus U+0897 has the key `rn` in TypeScript and none in Go. A test checks
395
- the blocks (`unicode_test.go`, `js/regression.test.ts`).
479
+ - **"The same output" holds inside the blocks indickit reads.** There,
480
+ both ports read one pinned table of Unicode 15.0 data
481
+ (`internal/unidata/unicode15.json`), whatever Unicode version the host
482
+ ships; a letter added since 15.0 is not a letter to indickit. Outside the
483
+ blocks (Latin accents, symbols), Go reads x/text's Unicode 15.0 and
484
+ TypeScript reads its host (node 24: 17.0), so a newer mark there can give
485
+ another output: "Ram" plus U+0897 has the key `rn` in TypeScript and none
486
+ in Go. Tests pin the table (`unicode_test.go`, `js/unidata.test.ts`).
487
+ The table costs about 2.8 KB gzipped in each browser file that reads it
488
+ (`normalize`, `phonetic`, `phonetic-search`, `romanize`, `deromanize`);
489
+ without it, those files would follow each browser's Unicode version.
396
490
  - **Two promises have a scope.** `normalize` twice gives what `normalize`
397
491
  once gives for text with at most 8 invisible characters; 9 BOMs before
398
492
  ૰ need a second call. `normalize` changes no `phonetic` key of a word
@@ -404,14 +498,14 @@ Each utility's own weak spots are at the end of its file in `docs/`.
404
498
 
405
499
  Every table and switch of a utility is in its rules file
406
500
  (`normalize/rules.json`, `stem/rules.json`, `phonetic/rules.json`,
407
- `segment/rules.json`; `romanize/rules.json` and its tables in
501
+ `segment/rules.json`, `wellformed/rules.json`; `romanize/rules.json` and its tables in
408
502
  `romanize/lang/`; `deromanize/rules.json` and its tables and word lists in
409
503
  `deromanize/lang/`; `phonetic-search`'s tables in `phonetic/scorer/`); the
410
504
  Go and TypeScript code is a short loop over it.
411
505
  Both are checked against a conformance file of inputs with the outputs that
412
506
  a reference implementation gave them: 345,276 inputs for `normalize`,
413
507
  418,665 for `stem`, 195,994 for `phonetic`, 858,654 for `segment`, 2,397
414
- for `romanize`, 2,818 for `deromanize`, and 1,021 search rows for
508
+ for `romanize`, 2,990 for `deromanize`, 32,279 for `wellformed`, and 1,021 search rows for
415
509
  `phonetic-search` (`*/testdata/*conformance.jsonl.gz`). A change that makes any one disagree
416
510
  on any input fails the build.
417
511