tdk-api-wrapper 1.3.1 → 1.5.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.
@@ -25,6 +25,559 @@ var TDKNetworkError = class extends TDKError {
25
25
  }
26
26
  };
27
27
 
28
+ // src/morphology.ts
29
+ var TURKISH_VOWELS = "ae\u0131io\xF6u\xFC";
30
+ function isVowel(ch) {
31
+ return TURKISH_VOWELS.includes(ch);
32
+ }
33
+ var TURKISH_SUFFIXES = [
34
+ // 9-letter composite suffixes
35
+ "lerimizden",
36
+ "lar\u0131m\u0131zdan",
37
+ "lerinizden",
38
+ "lar\u0131n\u0131zdan",
39
+ // 8-letter composite suffixes
40
+ "lerinin",
41
+ "lar\u0131n\u0131n",
42
+ "lerinde",
43
+ "lar\u0131nda",
44
+ "lerinden",
45
+ "lar\u0131ndan",
46
+ "leriyle",
47
+ "lar\u0131yla",
48
+ "lerini",
49
+ "lar\u0131n\u0131",
50
+ "lerimize",
51
+ "lar\u0131m\u0131za",
52
+ "lerimizle",
53
+ "lar\u0131m\u0131zla",
54
+ "lerinizin",
55
+ "lar\u0131n\u0131z\u0131n",
56
+ "lerinizde",
57
+ "lar\u0131n\u0131zda",
58
+ "d\u0131\u011F\u0131ndan",
59
+ "di\u011Finden",
60
+ "du\u011Fundan",
61
+ "d\xFC\u011F\xFCnden",
62
+ "t\u0131\u011F\u0131ndan",
63
+ "ti\u011Finden",
64
+ "tu\u011Fundan",
65
+ "t\xFC\u011F\xFCnden",
66
+ // 7-letter composite suffixes
67
+ "ecektir",
68
+ "acakt\u0131r",
69
+ "ece\u011Fim",
70
+ "aca\u011F\u0131m",
71
+ "eceksin",
72
+ "acaks\u0131n",
73
+ "ece\u011Fiz",
74
+ "aca\u011F\u0131z",
75
+ "lerimiz",
76
+ "lar\u0131m\u0131z",
77
+ "leriniz",
78
+ "lar\u0131n\u0131z",
79
+ "umuzdan",
80
+ "\xFCm\xFCzden",
81
+ "inizden",
82
+ "\u0131n\u0131zdan",
83
+ "\xFCn\xFCzden",
84
+ "d\u0131\u011F\u0131nda",
85
+ "di\u011Finde",
86
+ "du\u011Funda",
87
+ "d\xFC\u011F\xFCnde",
88
+ "t\u0131\u011F\u0131nda",
89
+ "ti\u011Finde",
90
+ "tu\u011Funda",
91
+ "t\xFC\u011F\xFCnde",
92
+ "mas\u0131na",
93
+ "mesine",
94
+ "\u0131yorsunuz",
95
+ "iyorsunuz",
96
+ "uyorsunuz",
97
+ "\xFCyorsunuz",
98
+ "yorsunuz",
99
+ // 6-letter composite suffixes
100
+ "iyorsa",
101
+ "iyorduk",
102
+ "iyordu",
103
+ "iyormu\u015F",
104
+ "\u0131yorsa",
105
+ "\u0131yorduk",
106
+ "\u0131yordu",
107
+ "\u0131yormu\u015F",
108
+ "uyorsa",
109
+ "uyorduk",
110
+ "uyordu",
111
+ "uyormu\u015F",
112
+ "\xFCyorsa",
113
+ "\xFCyorduk",
114
+ "\xFCyordu",
115
+ "\xFCyormu\u015F",
116
+ "\u0131yorsun",
117
+ "iyorsun",
118
+ "uyorsun",
119
+ "\xFCyorsun",
120
+ "\u0131yorlar",
121
+ "iyorlar",
122
+ "uyorlar",
123
+ "\xFCyorlar",
124
+ "iyoruz",
125
+ "\u0131yoruz",
126
+ "uyoruz",
127
+ "\xFCyoruz",
128
+ "imizin",
129
+ "\u0131m\u0131z\u0131n",
130
+ "umuzun",
131
+ "\xFCm\xFCz\xFCn",
132
+ "imizde",
133
+ "\u0131m\u0131zda",
134
+ "umuzda",
135
+ "\xFCm\xFCzde",
136
+ "imizden",
137
+ "\u0131m\u0131zdan",
138
+ "imizle",
139
+ "\u0131m\u0131zla",
140
+ "umuzla",
141
+ "\xFCm\xFCzle",
142
+ "lerdir",
143
+ "lard\u0131r",
144
+ "mu\u015Ftur",
145
+ "mi\u015Ftir",
146
+ "mu\u015Ftur",
147
+ "m\xFC\u015Ft\xFCr",
148
+ "lerden",
149
+ "lardan",
150
+ "lerine",
151
+ "lar\u0131na",
152
+ "leriyle",
153
+ "lar\u0131yla",
154
+ "seniz",
155
+ "san\u0131z",
156
+ "diniz",
157
+ "d\u0131n\u0131z",
158
+ "dunuz",
159
+ "d\xFCn\xFCz",
160
+ "tiniz",
161
+ "t\u0131n\u0131z",
162
+ "tunuz",
163
+ "t\xFCn\xFCz",
164
+ "siniz",
165
+ "s\u0131n\u0131z",
166
+ "sunuz",
167
+ "s\xFCn\xFCz",
168
+ "d\u0131k\xE7a",
169
+ "dik\xE7e",
170
+ "duk\xE7a",
171
+ "d\xFCk\xE7e",
172
+ "t\u0131k\xE7a",
173
+ "tik\xE7e",
174
+ "tuk\xE7a",
175
+ "t\xFCk\xE7e",
176
+ "\u0131rken",
177
+ "irken",
178
+ "urken",
179
+ "\xFCrken",
180
+ "arken",
181
+ "erken",
182
+ // 5-letter suffixes
183
+ "lerde",
184
+ "larda",
185
+ "lerle",
186
+ "larla",
187
+ "lerin",
188
+ "lar\u0131n",
189
+ "lerim",
190
+ "lar\u0131m",
191
+ "dirler",
192
+ "d\u0131rlar",
193
+ "d\xFCrler",
194
+ "durlar",
195
+ "tirler",
196
+ "t\u0131rlar",
197
+ "t\xFCrler",
198
+ "turlar",
199
+ "siniz",
200
+ "s\u0131n\u0131z",
201
+ "sunuz",
202
+ "s\xFCn\xFCz",
203
+ "yorum",
204
+ "yorsun",
205
+ "uyoruz",
206
+ "yorsunuz",
207
+ "yorlar",
208
+ "eceks",
209
+ "acaks",
210
+ "eyim",
211
+ "ay\u0131m",
212
+ "indik",
213
+ "\u0131nd\u0131k",
214
+ "unduk",
215
+ "\xFCnd\xFCk",
216
+ "ildik",
217
+ "\u0131ld\u0131k",
218
+ "ulduk",
219
+ "\xFCld\xFCk",
220
+ "meden",
221
+ "madan",
222
+ "y\u0131n\u0131z",
223
+ "yiniz",
224
+ "yunuz",
225
+ "y\xFCn\xFCz",
226
+ // 4-letter suffixes
227
+ "imiz",
228
+ "\u0131m\u0131z",
229
+ "umuz",
230
+ "\xFCm\xFCz",
231
+ "iniz",
232
+ "\u0131n\u0131z",
233
+ "unuz",
234
+ "\xFCn\xFCz",
235
+ "leri",
236
+ "lar\u0131",
237
+ "idir",
238
+ "\u0131d\u0131r",
239
+ "udur",
240
+ "\xFCd\xFCr",
241
+ "ecek",
242
+ "acak",
243
+ "erek",
244
+ "arak",
245
+ "ince",
246
+ "\u0131nca",
247
+ "unca",
248
+ "\xFCnce",
249
+ "ken",
250
+ "meli",
251
+ "mal\u0131",
252
+ "iyor",
253
+ "\u0131yor",
254
+ "uyor",
255
+ "\xFCyor",
256
+ "mi\u015Fti",
257
+ "m\u0131\u015Ft\u0131",
258
+ "mu\u015Ftu",
259
+ "m\xFC\u015Ft\xFC",
260
+ "seydi",
261
+ "sayd\u0131",
262
+ "ydim",
263
+ "yd\u0131m",
264
+ "ydum",
265
+ "yd\xFCm",
266
+ "tiler",
267
+ "t\u0131lar",
268
+ "diler",
269
+ "d\u0131lar",
270
+ "ikten",
271
+ "\u0131ktan",
272
+ "uktan",
273
+ "\xFCkten",
274
+ // 3-letter suffixes
275
+ "ler",
276
+ "lar",
277
+ "den",
278
+ "dan",
279
+ "ten",
280
+ "tan",
281
+ "dir",
282
+ "d\u0131r",
283
+ "dur",
284
+ "d\xFCr",
285
+ "tir",
286
+ "t\u0131r",
287
+ "tur",
288
+ "t\xFCr",
289
+ "nin",
290
+ "n\u0131n",
291
+ "nun",
292
+ "n\xFCn",
293
+ "yle",
294
+ "yla",
295
+ "mi\u015F",
296
+ "m\u0131\u015F",
297
+ "mu\u015F",
298
+ "m\xFC\u015F",
299
+ "dim",
300
+ "d\u0131m",
301
+ "dum",
302
+ "d\xFCm",
303
+ "tim",
304
+ "t\u0131m",
305
+ "tum",
306
+ "t\xFCm",
307
+ "din",
308
+ "d\u0131n",
309
+ "dun",
310
+ "d\xFCn",
311
+ "tin",
312
+ "t\u0131n",
313
+ "tun",
314
+ "t\xFCn",
315
+ "dik",
316
+ "d\u0131k",
317
+ "duk",
318
+ "d\xFCk",
319
+ "tik",
320
+ "t\u0131k",
321
+ "tuk",
322
+ "t\xFCk",
323
+ "ydi",
324
+ "yd\u0131",
325
+ "ydu",
326
+ "yd\xFC",
327
+ "yim",
328
+ "y\u0131m",
329
+ "yum",
330
+ "y\xFCm",
331
+ "sin",
332
+ "s\u0131n",
333
+ "sun",
334
+ "s\xFCn",
335
+ "sen",
336
+ "san",
337
+ "sem",
338
+ "sam",
339
+ "sek",
340
+ "sak",
341
+ "siz",
342
+ "s\u0131z",
343
+ "suz",
344
+ "s\xFCz",
345
+ "lik",
346
+ "l\u0131k",
347
+ "luk",
348
+ "l\xFCk",
349
+ "ici",
350
+ "\u0131c\u0131",
351
+ "ucu",
352
+ "\xFCc\xFC",
353
+ "gen",
354
+ "gan",
355
+ "ken",
356
+ "kan",
357
+ "len",
358
+ "lan",
359
+ "le\u015F",
360
+ "la\u015F",
361
+ "mek",
362
+ "mak",
363
+ "yor",
364
+ // 2-letter suffixes
365
+ "de",
366
+ "da",
367
+ "te",
368
+ "ta",
369
+ "im",
370
+ "\u0131m",
371
+ "um",
372
+ "\xFCm",
373
+ "in",
374
+ "\u0131n",
375
+ "un",
376
+ "\xFCn",
377
+ "iz",
378
+ "\u0131z",
379
+ "uz",
380
+ "\xFCz",
381
+ "si",
382
+ "s\u0131",
383
+ "su",
384
+ "s\xFC",
385
+ "ye",
386
+ "ya",
387
+ "le",
388
+ "la",
389
+ "di",
390
+ "d\u0131",
391
+ "du",
392
+ "d\xFC",
393
+ "ti",
394
+ "t\u0131",
395
+ "tu",
396
+ "t\xFC",
397
+ "se",
398
+ "sa",
399
+ "ce",
400
+ "ca",
401
+ "\xE7e",
402
+ "\xE7a",
403
+ "me",
404
+ "ma",
405
+ "ip",
406
+ "\u0131p",
407
+ "up",
408
+ "\xFCp",
409
+ "en",
410
+ "an",
411
+ "i\u015F",
412
+ "\u0131\u015F",
413
+ "u\u015F",
414
+ "\xFC\u015F",
415
+ "li",
416
+ "l\u0131",
417
+ "lu",
418
+ "l\xFC",
419
+ "ci",
420
+ "c\u0131",
421
+ "cu",
422
+ "c\xFC",
423
+ "\xE7i",
424
+ "\xE7\u0131",
425
+ "\xE7u",
426
+ "\xE7\xFC",
427
+ // 1-letter suffixes (vowels / basic case endings)
428
+ "e",
429
+ "a",
430
+ "i",
431
+ "\u0131",
432
+ "u",
433
+ "\xFC"
434
+ ];
435
+ function restoreConsonantSoftening(stem) {
436
+ if (stem.length < 2)
437
+ return [];
438
+ const last = stem.slice(-1);
439
+ const base = stem.slice(0, -1);
440
+ switch (last) {
441
+ case "b":
442
+ return [base + "p"];
443
+ case "c":
444
+ return [base + "\xE7"];
445
+ case "d":
446
+ return [base + "t"];
447
+ case "\u011F":
448
+ return [base + "k"];
449
+ case "g":
450
+ return [base + "k"];
451
+ default:
452
+ return [];
453
+ }
454
+ }
455
+ function restoreVowelDrop(stem) {
456
+ if (stem.length < 3)
457
+ return [];
458
+ const c1 = stem[stem.length - 2];
459
+ const c2 = stem[stem.length - 1];
460
+ if (!isVowel(c1) && !isVowel(c2)) {
461
+ const vowelsInBase = stem.slice(0, -2).split("").filter(isVowel);
462
+ if (vowelsInBase.length > 0) {
463
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
464
+ let inserted = "i";
465
+ if ("a\u0131".includes(lastVowel))
466
+ inserted = "\u0131";
467
+ else if ("ei".includes(lastVowel))
468
+ inserted = "i";
469
+ else if ("ou".includes(lastVowel))
470
+ inserted = "u";
471
+ else if ("\xF6\xFC".includes(lastVowel))
472
+ inserted = "\xFC";
473
+ return [stem.slice(0, -1) + inserted + c2];
474
+ }
475
+ }
476
+ return [];
477
+ }
478
+ function restoreGemination(stem) {
479
+ if (stem.length < 3)
480
+ return [];
481
+ const c1 = stem[stem.length - 2];
482
+ const c2 = stem[stem.length - 1];
483
+ if (c1 === c2 && !isVowel(c1)) {
484
+ const single = stem.slice(0, -1);
485
+ const hardened = restoreConsonantSoftening(single);
486
+ return [single, ...hardened];
487
+ }
488
+ return [];
489
+ }
490
+ function restoreVowelNarrowing(stem) {
491
+ if (stem.length < 2)
492
+ return [];
493
+ if (stem === "di")
494
+ return ["de"];
495
+ if (stem === "yi")
496
+ return ["ye"];
497
+ const lastChar = stem[stem.length - 1];
498
+ const isLastNarrow = "\u0131iu\xFC".includes(lastChar);
499
+ if (isLastNarrow) {
500
+ const vowelsInBase = stem.slice(0, -1).split("").filter(isVowel);
501
+ const lastVowel = vowelsInBase.length > 0 ? vowelsInBase[vowelsInBase.length - 1] : lastChar;
502
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
503
+ return [stem.slice(0, -1) + widened];
504
+ }
505
+ if (!isVowel(lastChar)) {
506
+ const vowelsInBase = stem.split("").filter(isVowel);
507
+ if (vowelsInBase.length > 0) {
508
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
509
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
510
+ return [stem + widened];
511
+ }
512
+ }
513
+ return [];
514
+ }
515
+ function restoreInfinitive(stem) {
516
+ if (stem.length < 2)
517
+ return [];
518
+ const vowelsInBase = stem.split("").filter(isVowel);
519
+ if (vowelsInBase.length === 0)
520
+ return [];
521
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
522
+ return "a\u0131ou".includes(lastVowel) ? [stem + "mak"] : [stem + "mek"];
523
+ }
524
+ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
525
+ if (!word || word.trim().length === 0)
526
+ return [];
527
+ const raw = word.trim();
528
+ const normalized = raw.toLocaleLowerCase("tr-TR");
529
+ const candidatesWithWeight = [];
530
+ const seen = /* @__PURE__ */ new Set();
531
+ if (raw.includes("'") || raw.includes("\u2019")) {
532
+ const apostropheStem = normalized.split(/['’]/)[0];
533
+ if (apostropheStem.length >= minStemLength) {
534
+ candidatesWithWeight.push({ candidate: apostropheStem, baseLength: apostropheStem.length + 10 });
535
+ seen.add(apostropheStem);
536
+ }
537
+ }
538
+ let frontier = [normalized];
539
+ for (let depth = 0; depth < maxDepth; depth++) {
540
+ const nextFrontier = [];
541
+ for (const current of frontier) {
542
+ for (const suffix of TURKISH_SUFFIXES) {
543
+ if (current.length - suffix.length >= minStemLength && current.endsWith(suffix)) {
544
+ const stem = current.slice(0, -suffix.length);
545
+ const hardened = restoreConsonantSoftening(stem);
546
+ const vowelDropped = restoreVowelDrop(stem);
547
+ const geminated = restoreGemination(stem);
548
+ const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
549
+ const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
550
+ const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
551
+ const isVerbSuffix = isNarrowingSuffix || suffix.includes("ecek") || suffix.includes("acak") || suffix.includes("mi\u015F") || suffix.includes("m\u0131\u015F") || suffix.includes("m\xFC\u015F") || suffix.includes("mu\u015F") || suffix.includes("mek") || suffix.includes("mak") || suffix.includes("erek") || suffix.includes("arak") || suffix.includes("dik") || suffix.includes("d\u0131k") || suffix.includes("duk") || suffix.includes("d\xFCk") || suffix.includes("tik") || suffix.includes("t\u0131k") || suffix.includes("tuk") || suffix.includes("t\xFCk") || suffix.includes("sen") || suffix.includes("san") || suffix.includes("sem") || suffix.includes("sam") || suffix.includes("sek") || suffix.includes("sak");
552
+ const verbalBases = [stem, ...hardened, ...narrowed];
553
+ const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
554
+ const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
555
+ for (const variant of variants) {
556
+ if (!seen.has(variant) && variant !== normalized) {
557
+ seen.add(variant);
558
+ nextFrontier.push(variant);
559
+ candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
560
+ }
561
+ }
562
+ for (const inf of infinitives) {
563
+ if (!seen.has(inf) && inf !== normalized) {
564
+ seen.add(inf);
565
+ nextFrontier.push(inf);
566
+ const weight = isVerbSuffix ? stem.length + 5 : stem.length;
567
+ candidatesWithWeight.push({ candidate: inf, baseLength: weight });
568
+ }
569
+ }
570
+ }
571
+ }
572
+ }
573
+ if (nextFrontier.length === 0)
574
+ break;
575
+ frontier = nextFrontier;
576
+ }
577
+ candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
578
+ return [...new Set(candidatesWithWeight.map((c) => c.candidate))];
579
+ }
580
+
28
581
  // src/tdk.ts
29
582
  import * as fs from "fs";
30
583
  import * as path from "path";
@@ -111,11 +664,30 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
111
664
  yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
112
665
  -----END CERTIFICATE-----`
113
666
  ];
667
+ // Configuration
668
+ static defaultTimeoutMs = 8e3;
669
+ static defaultRetries = 1;
670
+ static maxCacheSize = 1e3;
114
671
  // Cache Mechanism
115
672
  static isCacheEnabled = false;
116
673
  static wordCache = /* @__PURE__ */ new Map();
117
674
  static dailyContentCache = null;
118
675
  static autocompleteCache = [];
676
+ static autocompleteSet = /* @__PURE__ */ new Set();
677
+ static stemCache = /* @__PURE__ */ new Map();
678
+ /**
679
+ * Configures global client options such as network timeout, retries, and cache size.
680
+ */
681
+ static configure(config) {
682
+ if (config.timeoutMs !== void 0)
683
+ this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
684
+ if (config.retries !== void 0)
685
+ this.defaultRetries = Math.max(0, config.retries);
686
+ if (config.cache !== void 0)
687
+ this.enableCache(config.cache);
688
+ if (config.maxCacheSize !== void 0)
689
+ this.maxCacheSize = Math.max(10, config.maxCacheSize);
690
+ }
119
691
  /**
120
692
  * Enables or disables in-memory caching for API requests.
121
693
  */
@@ -132,10 +704,53 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
132
704
  this.wordCache.clear();
133
705
  this.dailyContentCache = null;
134
706
  this.autocompleteCache = [];
707
+ this.autocompleteSet.clear();
708
+ this.stemCache.clear();
709
+ }
710
+ static setBoundedCache(map, key, value) {
711
+ if (map.size >= this.maxCacheSize) {
712
+ const firstKey = map.keys().next().value;
713
+ if (firstKey !== void 0)
714
+ map.delete(firstKey);
715
+ }
716
+ map.set(key, value);
135
717
  }
136
718
  static delay(ms) {
137
719
  return new Promise((resolve) => setTimeout(resolve, ms));
138
720
  }
721
+ /**
722
+ * Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
723
+ */
724
+ static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
725
+ let lastError;
726
+ for (let attempt = 0; attempt <= retries; attempt++) {
727
+ try {
728
+ const signal = AbortSignal.timeout(timeoutMs);
729
+ const headers = {
730
+ "User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
731
+ ...options.headers || {}
732
+ };
733
+ const res = await fetch(url, { ...options, headers, signal });
734
+ if (res.ok || res.status >= 400 && res.status < 500) {
735
+ return res;
736
+ }
737
+ if (attempt < retries) {
738
+ await this.delay(200 * (attempt + 1));
739
+ continue;
740
+ }
741
+ return res;
742
+ } catch (err) {
743
+ lastError = err;
744
+ if (attempt < retries) {
745
+ await this.delay(200 * (attempt + 1));
746
+ continue;
747
+ }
748
+ }
749
+ }
750
+ throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
751
+ cause: lastError
752
+ });
753
+ }
139
754
  /**
140
755
  * Fetches detailed information for a given word from the TDK Dictionary.
141
756
  */
@@ -150,9 +765,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
150
765
  const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
151
766
  let response;
152
767
  try {
153
- response = await fetch(url, {
154
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
155
- });
768
+ response = await this.fetchWithRetry(url);
156
769
  } catch (error) {
157
770
  throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
158
771
  }
@@ -169,12 +782,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
169
782
  }
170
783
  if (!Array.isArray(data) && data && "error" in data) {
171
784
  if (this.isCacheEnabled)
172
- this.wordCache.set(cleanWord, []);
785
+ this.setBoundedCache(this.wordCache, cleanWord, []);
173
786
  return [];
174
787
  }
175
788
  const results = data;
176
789
  if (this.isCacheEnabled) {
177
- this.wordCache.set(cleanWord, results);
790
+ this.setBoundedCache(this.wordCache, cleanWord, results);
178
791
  }
179
792
  return results;
180
793
  }
@@ -238,6 +851,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
238
851
  return [];
239
852
  }
240
853
  }
854
+ /**
855
+ * Ensures TDK's ~81k headword list is loaded in memory for fast O(1) set operations.
856
+ */
857
+ static async ensureAutocompleteLoaded() {
858
+ if (this.autocompleteCache.length === 0) {
859
+ this.autocompleteCache = await this.fetchAutocompleteData();
860
+ this.autocompleteSet = new Set(
861
+ this.autocompleteCache.map((w) => w.toLocaleLowerCase("tr-TR"))
862
+ );
863
+ }
864
+ }
241
865
  /**
242
866
  * Returns autocomplete suggestions for a given prefix, searched over TDK's
243
867
  * full headword list (see `fetchAutocompleteData`). The list is fetched
@@ -247,12 +871,81 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
247
871
  static async getSuggestions(prefix) {
248
872
  if (!prefix || prefix.trim() === "")
249
873
  return [];
250
- if (this.autocompleteCache.length === 0) {
251
- this.autocompleteCache = await this.fetchAutocompleteData();
252
- }
874
+ await this.ensureAutocompleteLoaded();
253
875
  const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
254
876
  return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
255
877
  }
878
+ /**
879
+ * Checks whether a word exists as a known headword in TDK dictionary.
880
+ * Checks in-memory autocompleteSet (81k headwords) if loaded, or queries TDK API.
881
+ */
882
+ static async isHeadword(word) {
883
+ if (!word || word.trim() === "")
884
+ return false;
885
+ const clean = word.trim().toLocaleLowerCase("tr-TR");
886
+ await this.ensureAutocompleteLoaded();
887
+ if (this.autocompleteSet.size > 0) {
888
+ return this.autocompleteSet.has(clean);
889
+ }
890
+ try {
891
+ const results = await this.getWord(clean);
892
+ return results.length > 0;
893
+ } catch {
894
+ return false;
895
+ }
896
+ }
897
+ /**
898
+ * Generates candidate roots for a given Turkish word using progressive BFS suffix stripping,
899
+ * consonant mutation restoration, and vowel drop restoration.
900
+ */
901
+ static getStemCandidates(word) {
902
+ return getStemCandidates(word);
903
+ }
904
+ /**
905
+ * Finds the dictionary root (headword) of a word by checking direct existence
906
+ * and evaluating candidate stems generated by morphological analysis.
907
+ * Returns the root headword string if found, or null if no match in TDK.
908
+ */
909
+ static async findRoot(word) {
910
+ if (!word || word.trim() === "")
911
+ return null;
912
+ const clean = word.trim().toLocaleLowerCase("tr-TR");
913
+ if (this.stemCache.has(clean)) {
914
+ return this.stemCache.get(clean);
915
+ }
916
+ if (await this.isHeadword(clean)) {
917
+ this.setBoundedCache(this.stemCache, clean, clean);
918
+ return clean;
919
+ }
920
+ const candidates = getStemCandidates(clean);
921
+ for (const candidate of candidates) {
922
+ if (await this.isHeadword(candidate)) {
923
+ this.setBoundedCache(this.stemCache, clean, candidate);
924
+ return candidate;
925
+ }
926
+ }
927
+ this.setBoundedCache(this.stemCache, clean, null);
928
+ return null;
929
+ }
930
+ /**
931
+ * Performs morphological stemming on a Turkish word.
932
+ * Returns a StemResult containing the original word, resolved root, and whether it is inflected.
933
+ */
934
+ static async stem(word) {
935
+ if (!word || word.trim() === "")
936
+ return null;
937
+ const clean = word.trim().toLocaleLowerCase("tr-TR");
938
+ const root = await this.findRoot(word);
939
+ if (!root) {
940
+ return null;
941
+ }
942
+ return {
943
+ word,
944
+ root,
945
+ isInflected: root !== clean,
946
+ candidates: getStemCandidates(word)
947
+ };
948
+ }
256
949
  /**
257
950
  * Returns a list of proverbs and idioms containing the word.
258
951
  */
@@ -469,6 +1162,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
469
1162
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
470
1163
  }
471
1164
  }
1165
+ const root = await this.findRoot(word);
1166
+ if (root) {
1167
+ const cleanWord2 = word.trim().toLocaleLowerCase("tr-TR");
1168
+ const isInflected = root !== cleanWord2;
1169
+ return {
1170
+ isCorrect: true,
1171
+ word,
1172
+ isInflected,
1173
+ root
1174
+ };
1175
+ }
472
1176
  if (this.autocompleteCache.length === 0) {
473
1177
  this.autocompleteCache = await this.fetchAutocompleteData();
474
1178
  }
@@ -896,14 +1600,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
896
1600
  meaningCount: meaningsA.length,
897
1601
  origin: originA,
898
1602
  syllables: this.syllabicate(a),
899
- harmony: this.checkVowelHarmony(a)
1603
+ harmony: this.checkVowelHarmony(a),
1604
+ labialHarmony: this.checkLabialHarmony(a)
900
1605
  },
901
1606
  b: {
902
1607
  word: b,
903
1608
  meaningCount: meaningsB.length,
904
1609
  origin: originB,
905
1610
  syllables: this.syllabicate(b),
906
- harmony: this.checkVowelHarmony(b)
1611
+ harmony: this.checkVowelHarmony(b),
1612
+ labialHarmony: this.checkLabialHarmony(b)
907
1613
  }
908
1614
  };
909
1615
  }
@@ -965,13 +1671,28 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
965
1671
  const unique = [...new Set(words)];
966
1672
  const analyses = [];
967
1673
  for (const word of unique) {
968
- const results = await this.getWord(word);
969
- const found = results.length > 0;
1674
+ let results = await this.getWord(word);
1675
+ let found = results.length > 0;
1676
+ let root;
1677
+ let isInflected;
1678
+ if (!found) {
1679
+ const resolvedRoot = await this.findRoot(word);
1680
+ if (resolvedRoot) {
1681
+ results = await this.getWord(resolvedRoot);
1682
+ if (results.length > 0) {
1683
+ found = true;
1684
+ root = resolvedRoot;
1685
+ isInflected = true;
1686
+ }
1687
+ }
1688
+ }
970
1689
  analyses.push({
971
1690
  word,
972
1691
  found,
973
1692
  meaning: found ? this.firstMeaning(results) : null,
974
- origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null
1693
+ origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null,
1694
+ root,
1695
+ isInflected
975
1696
  });
976
1697
  await this.delay(200);
977
1698
  }
@@ -1019,9 +1740,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1019
1740
  }
1020
1741
  /**
1021
1742
  * Syllabicates a Turkish word based on general grammar rules.
1743
+ * Handles syllable separation for vowels, single consonants, double consonants,
1744
+ * and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
1022
1745
  */
1023
1746
  static syllabicate(word) {
1024
1747
  const vowels = /[aeıioöuüAEIİOÖUÜ]/;
1748
+ const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
1025
1749
  const result = [];
1026
1750
  let currentSyllable = "";
1027
1751
  for (let i = word.length - 1; i >= 0; i--) {
@@ -1032,8 +1756,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1032
1756
  currentSyllable = word[i - 1] + currentSyllable;
1033
1757
  i--;
1034
1758
  } else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
1035
- currentSyllable = word[i - 1] + currentSyllable;
1036
- i--;
1759
+ if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
1760
+ currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
1761
+ i -= 2;
1762
+ } else {
1763
+ currentSyllable = word[i - 1] + currentSyllable;
1764
+ i--;
1765
+ }
1037
1766
  }
1038
1767
  }
1039
1768
  result.unshift(currentSyllable);
@@ -1063,11 +1792,307 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1063
1792
  const hasFront = frontVowels.test(lower);
1064
1793
  return !(hasBack && hasFront);
1065
1794
  }
1795
+ /**
1796
+ * Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
1797
+ * Rules:
1798
+ * 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
1799
+ * 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
1800
+ * Single-syllable words and words with <=1 vowel are considered compliant by convention.
1801
+ */
1802
+ static checkLabialHarmony(word) {
1803
+ const lower = word.toLocaleLowerCase("tr-TR");
1804
+ const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
1805
+ if (vowels.length <= 1)
1806
+ return true;
1807
+ for (let i = 0; i < vowels.length - 1; i++) {
1808
+ const v1 = vowels[i];
1809
+ const v2 = vowels[i + 1];
1810
+ if ("ae\u0131i".includes(v1)) {
1811
+ if (!"ae\u0131i".includes(v2))
1812
+ return false;
1813
+ } else if ("o\xF6u\xFC".includes(v1)) {
1814
+ if (!"aeu\xFC".includes(v2))
1815
+ return false;
1816
+ }
1817
+ }
1818
+ return true;
1819
+ }
1820
+ /**
1821
+ * Searches TDK headwords using a wildcard / pattern string.
1822
+ * Wildcards:
1823
+ * '_' or '?' matches any single character
1824
+ * '*' matches zero or more characters
1825
+ * Example: "k_l_m" matches "kalem", "kelam", "kilim".
1826
+ * Runs in-memory against TDK's 81k headword list.
1827
+ */
1828
+ static async patternSearch(pattern, options) {
1829
+ if (!pattern || pattern.trim() === "")
1830
+ return [];
1831
+ await this.ensureAutocompleteLoaded();
1832
+ const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
1833
+ const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
1834
+ const regex = new RegExp(`^${escaped}$`, "u");
1835
+ const max = options?.maxResults ?? 50;
1836
+ const matches = [];
1837
+ for (const headword of this.autocompleteCache) {
1838
+ const lower = headword.toLocaleLowerCase("tr-TR");
1839
+ if (regex.test(lower)) {
1840
+ matches.push(headword);
1841
+ if (matches.length >= max)
1842
+ break;
1843
+ }
1844
+ }
1845
+ return matches;
1846
+ }
1847
+ /**
1848
+ * Finds headwords in TDK that can be formed from the given letters (anagrams).
1849
+ * If exact-length anagrams exist, they are returned.
1850
+ * If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
1851
+ * minimum 3 letters) are returned, sorted by length descending.
1852
+ */
1853
+ static async findAnagrams(letters, options) {
1854
+ if (!letters || letters.trim() === "")
1855
+ return [];
1856
+ await this.ensureAutocompleteLoaded();
1857
+ const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
1858
+ if (clean.length === 0)
1859
+ return [];
1860
+ const forceExact = options?.exactLength === true;
1861
+ const max = options?.maxResults ?? 50;
1862
+ const getFrequency = (str) => {
1863
+ const freq = {};
1864
+ for (const ch of str) {
1865
+ freq[ch] = (freq[ch] || 0) + 1;
1866
+ }
1867
+ return freq;
1868
+ };
1869
+ const targetFreq = getFrequency(clean);
1870
+ const exactMatches = [];
1871
+ const subMatches = [];
1872
+ for (const headword of this.autocompleteCache) {
1873
+ const lower = headword.toLocaleLowerCase("tr-TR");
1874
+ if (lower.includes(" ") || lower.includes("-"))
1875
+ continue;
1876
+ if (lower.length > clean.length || lower.length < 3)
1877
+ continue;
1878
+ const wordFreq = getFrequency(lower);
1879
+ let isValid = true;
1880
+ for (const [ch, count] of Object.entries(wordFreq)) {
1881
+ if (!targetFreq[ch] || targetFreq[ch] < count) {
1882
+ isValid = false;
1883
+ break;
1884
+ }
1885
+ }
1886
+ if (isValid && lower !== clean) {
1887
+ if (lower.length === clean.length) {
1888
+ exactMatches.push(headword);
1889
+ } else {
1890
+ subMatches.push(headword);
1891
+ }
1892
+ }
1893
+ }
1894
+ if (exactMatches.length > 0 || forceExact) {
1895
+ return exactMatches.slice(0, max);
1896
+ }
1897
+ subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
1898
+ return subMatches.slice(0, max);
1899
+ }
1900
+ /**
1901
+ * Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
1902
+ * @param word The target word
1903
+ * @param options.minLetters Minimum number of ending characters that must match (default: 3)
1904
+ * @param options.maxResults Maximum number of rhyme results to return (default: 50)
1905
+ */
1906
+ static async findRhymes(word, options) {
1907
+ if (!word || word.trim() === "")
1908
+ return [];
1909
+ await this.ensureAutocompleteLoaded();
1910
+ const clean = word.trim().toLocaleLowerCase("tr-TR");
1911
+ const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
1912
+ const max = options?.maxResults ?? 50;
1913
+ const suffix = clean.slice(-minLetters);
1914
+ const results = [];
1915
+ for (const headword of this.autocompleteCache) {
1916
+ const lower = headword.toLocaleLowerCase("tr-TR");
1917
+ if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
1918
+ results.push(headword);
1919
+ if (results.length >= max)
1920
+ break;
1921
+ }
1922
+ }
1923
+ return results;
1924
+ }
1925
+ /**
1926
+ * Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
1927
+ * Detects:
1928
+ * 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
1929
+ * 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
1930
+ * 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
1931
+ * 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
1932
+ */
1933
+ static async proofread(text) {
1934
+ if (!text || text.trim() === "") {
1935
+ return { text: text || "", issues: [], isCorrect: true };
1936
+ }
1937
+ await this.ensureAutocompleteLoaded();
1938
+ const issues = [];
1939
+ const SOMBAHCEMI = /* @__PURE__ */ new Set([
1940
+ "sanki",
1941
+ "oysaki",
1942
+ "mademki",
1943
+ "belki",
1944
+ "halbuki",
1945
+ "\xE7\xFCnk\xFC",
1946
+ "me\u011Ferki",
1947
+ "illaki"
1948
+ ]);
1949
+ const tokenRegex = /[\p{L}0-9'’]+/gu;
1950
+ let match;
1951
+ while ((match = tokenRegex.exec(text)) !== null) {
1952
+ const rawWord = match[0];
1953
+ const startIndex = match.index;
1954
+ const endIndex = startIndex + rawWord.length;
1955
+ const lower = rawWord.toLocaleLowerCase("tr-TR");
1956
+ if (/^\d+$/.test(lower))
1957
+ continue;
1958
+ let flagged = false;
1959
+ const questionMatch = lower.match(/^(.+?)(m[ıiuü](?:sin|sın|sun|sün|siniz|sınız|sunuz|sünüz|yiz|yız|yuz|yüz|m|k)?)$/);
1960
+ if (questionMatch) {
1961
+ const base = questionMatch[1];
1962
+ const particle = questionMatch[2];
1963
+ if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
1964
+ if (!await this.isHeadword(lower)) {
1965
+ issues.push({
1966
+ type: "question_particle",
1967
+ word: rawWord,
1968
+ startIndex,
1969
+ endIndex,
1970
+ suggestion: `${base} ${particle}`,
1971
+ message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
1972
+ });
1973
+ flagged = true;
1974
+ }
1975
+ }
1976
+ }
1977
+ const VERB_CONJUGATION_REGEX = /(?:d[ıiuü][kmmn]?|t[ıiuü][kmmn]?|d[ıiuü]n[ıiuü]z?|t[ıiuü]n[ıiuü]z?|m[ıiuü]ş(?:[szn][ıiuü]z?|lar)?|yor(?:um|sun|uz|lar)?|ecek(?:sin|iz|ler)?|acak(?:sın|ız|lar)?|s[ae][mnk]|s[ae]n[ıiz]?|meli|malı|me[mz]|ma[mz])$/i;
1978
+ if (!flagged && lower.endsWith("ki") && lower.length > 3) {
1979
+ const base = lower.slice(0, -2);
1980
+ if (!SOMBAHCEMI.has(lower)) {
1981
+ if (!await this.isHeadword(lower)) {
1982
+ const root = await this.findRoot(base);
1983
+ const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base);
1984
+ if (isVerb) {
1985
+ issues.push({
1986
+ type: "conjunction_ki",
1987
+ word: rawWord,
1988
+ startIndex,
1989
+ endIndex,
1990
+ suggestion: `${base} ki`,
1991
+ message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
1992
+ });
1993
+ flagged = true;
1994
+ }
1995
+ }
1996
+ }
1997
+ }
1998
+ if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
1999
+ const base = lower.slice(0, -2);
2000
+ const ending = lower.slice(-2);
2001
+ if (!await this.isHeadword(lower)) {
2002
+ const root = await this.findRoot(base);
2003
+ const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || VERB_CONJUGATION_REGEX.test(base);
2004
+ if (isVerb) {
2005
+ const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
2006
+ issues.push({
2007
+ type: "conjunction_da",
2008
+ word: rawWord,
2009
+ startIndex,
2010
+ endIndex,
2011
+ suggestion: `${base} ${correctEnding}`,
2012
+ message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
2013
+ });
2014
+ flagged = true;
2015
+ }
2016
+ }
2017
+ }
2018
+ if (!flagged) {
2019
+ const check = await this.checkSpelling(rawWord);
2020
+ if (!check.isCorrect) {
2021
+ issues.push({
2022
+ type: "spelling",
2023
+ word: rawWord,
2024
+ startIndex,
2025
+ endIndex,
2026
+ suggestion: check.suggestion,
2027
+ message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir. \xD6neri: '${check.suggestion}'` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
2028
+ });
2029
+ }
2030
+ }
2031
+ }
2032
+ return {
2033
+ text,
2034
+ issues,
2035
+ isCorrect: issues.length === 0
2036
+ };
2037
+ }
2038
+ };
2039
+ var TDKClient = class {
2040
+ constructor(config) {
2041
+ if (config) {
2042
+ TDK.configure(config);
2043
+ }
2044
+ }
2045
+ getWord(word) {
2046
+ return TDK.getWord(word);
2047
+ }
2048
+ getMeanings(word) {
2049
+ return TDK.getMeanings(word);
2050
+ }
2051
+ checkSpelling(word) {
2052
+ return TDK.checkSpelling(word);
2053
+ }
2054
+ findRoot(word) {
2055
+ return TDK.findRoot(word);
2056
+ }
2057
+ stem(word) {
2058
+ return TDK.stem(word);
2059
+ }
2060
+ proofread(text) {
2061
+ return TDK.proofread(text);
2062
+ }
2063
+ patternSearch(pattern, options) {
2064
+ return TDK.patternSearch(pattern, options);
2065
+ }
2066
+ findAnagrams(letters, options) {
2067
+ return TDK.findAnagrams(letters, options);
2068
+ }
2069
+ findRhymes(word, options) {
2070
+ return TDK.findRhymes(word, options);
2071
+ }
2072
+ syllabicate(word) {
2073
+ return TDK.syllabicate(word);
2074
+ }
2075
+ checkVowelHarmony(word) {
2076
+ return TDK.checkVowelHarmony(word);
2077
+ }
2078
+ checkLabialHarmony(word) {
2079
+ return TDK.checkLabialHarmony(word);
2080
+ }
1066
2081
  };
1067
2082
 
1068
2083
  export {
1069
2084
  TDKError,
1070
2085
  TDKValidationError,
1071
2086
  TDKNetworkError,
1072
- TDK
2087
+ TURKISH_VOWELS,
2088
+ isVowel,
2089
+ TURKISH_SUFFIXES,
2090
+ restoreConsonantSoftening,
2091
+ restoreVowelDrop,
2092
+ restoreGemination,
2093
+ restoreVowelNarrowing,
2094
+ restoreInfinitive,
2095
+ getStemCandidates,
2096
+ TDK,
2097
+ TDKClient
1073
2098
  };