tdk-api-wrapper 1.2.1 → 1.2.2

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
@@ -68,7 +68,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
68
68
  - **`TDK.syllabicate(word)`**: Kelimeyi Türkçe heceleme kurallarına göre doğru hecelerine ayırır (Örn: `['mu', 'vaf', 'fa', 'ki', 'yet']`). API isteği atmaz, çok hızlıdır.
69
69
  - **`TDK.checkVowelHarmony(word)`**: Kelimenin büyük ünlü uyumuna uyup uymadığını (boolean) kontrol eder.
70
70
  - **`TDK.getPartOfSpeech(word)`**: Kelimenin sözcük türünü (isim, sıfat, zarf vb.) döndürür.
71
- - **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'nin "sık yapılan yanlışlar" listesinde tam eşleşme arar; bulamazsa TDK'nin ~81 bin kelimelik tam madde listesi üzerinde edit-distance (Levenshtein) ile en yakın kelimeyi önerir (örn. `herkez` → `herkes`, `mektub` → `mektup`).
71
+ - **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'nin "sık yapılan yanlışlar" listesinde tam eşleşme arar; bulamazsa TDK'nin ~81 bin kelimelik tam madde listesi üzerinde Damerau-Levenshtein edit-distance ile en yakın kelimeyi önerir (bitişik harf yer değiştirmelerini de tek düzeltme sayar; örn. `herkez` → `herkes`, `mektub` → `mektup`, `yanlız` → `yalnız`). Kelime sıklığı verisi olmadığı için nadiren aynı mesafedeki iki aday arasında beklenenden farklı biri seçilebilir.
72
72
  - **`TDK.getCompoundWords(word)`**: Aranan kelime ile oluşturulmuş birleşik kelimeleri (Örn: dolma kalem) listeler.
73
73
 
74
74
  ### 3. Edebi ve Kültürel Analiz
@@ -79,7 +79,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
79
79
  - **`TDK.groupByOrigin(words)`**: Bir kelime listesini etimolojik kökenlerine göre gruplar (bulunamayanlar `"Bilinmiyor"` altında toplanır).
80
80
  - **`TDK.getSynonyms(word)`** / **`TDK.getAntonyms(word)`**: Kelimenin eş/zıt anlamlılarını döner (undocumented `gts-yeni` endpoint'i üzerinden; sonuç bulunamazsa `[]`).
81
81
  - **`TDK.compareWords(a, b)`**: İki kelimeyi anlam sayısı, köken, hece bölünüşü ve büyük ünlü uyumu açısından yan yana karşılaştırır.
82
- - **`TDK.analyzeText(text)`**: Bir metindeki (Türkçe bağlaçlar/edatlar hariç) her benzersiz kelimeyi tek tek arayıp ilk anlamını ve kökenini döner.
82
+ - **`TDK.analyzeText(text)`**: Bir metindeki (Türkçe bağlaçlar/edatlar hariç) her benzersiz kelimeyi tek tek arayıp ilk anlamını ve kökenini döner. Not: TDK yalnızca yalın (sözlük) biçimleri indeksliyor, morfolojik analiz yapmıyor — bu yüzden "evde", "dildir" gibi ek almış kelimeler kökleri (`ev`, `dil`) sözlükte olsa bile `found: false` döner; bu veri kaynağının doğal bir sınırlılığıdır.
83
83
 
84
84
  ### 4. Yardımcı Metotlar
85
85
  - **`TDK.getSuggestions(prefix)`**: TDK'nin ~81 bin kelimelik tam madde listesi üzerinden önek bazlı otomatik tamamlama önerileri döner (ilk çağrıda listeyi indirip önbelleğe alır, sonraki çağrılar anlıktır).
@@ -88,7 +88,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
88
88
  - **`TDK.getWordOfTheDay()`**: `getDailyContent()`'in üzerine ince bir katman; günün kelimesini ve tüm anlamlarını `{ word, meanings }` şeklinde döner.
89
89
  - **`TDK.getRandomWord()`**: Günün içeriğindeki kelime ve atasözü havuzundan rastgele bir tanesini `{ type: "kelime" | "atasoz", madde, anlam }` şeklinde seçer (not: tüm sözlük değil, sadece o günkü içerik havuzundan seçim yapar).
90
90
  - **`TDK.getKurallar()`**: TDK'nin `/icerik` akışının o an döndürdüğü yazım kuralı sayfa(lar)ını `{ adi, url }` şeklinde listeler. Not: bu sabit bir katalog değildir — `/icerik` her istekte, yaklaşık yirmi kurallık bir havuzdan rastgele tek bir kural döndürür.
91
- - **`TDK.getRule(name)`**: Adı verilen (küçük/büyük harf duyarsız, alt dize eşleşmesi) yazım kuralının tam metnini `tdk.gov.tr`'den çekip düz metne çevirir. `getKurallar()`'ın rastgeleliği yüzünden istenen kuralı bulana kadar sınırlı sayıda (en fazla 25, aralarda kısa bekleme ile) yeniden dener; bulamazsa veya sayfa ayrıştırılamazsa `null` döner.
91
+ - **`TDK.getRule(name)`**: Adı verilen (küçük/büyük harf duyarsız, alt dize eşleşmesi) yazım kuralının tam metnini `tdk.gov.tr`'den çekip düz metne çevirir. `getKurallar()`'ın rastgeleliği yüzünden istenen kuralı bulana kadar eşzamanlı gruplar hâlinde (toplam en fazla 25 deneme, ~5 round-trip'e sığdırılmış) yeniden dener; bulamazsa veya sayfa ayrıştırılamazsa `null` döner.
92
92
 
93
93
  ## Hata Yönetimi
94
94
 
@@ -399,11 +399,16 @@ var TDK = class {
399
399
  for (const candidate of this.autocompleteCache) {
400
400
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
401
401
  continue;
402
- const distance = this.levenshtein(cleanWord, candidate);
403
- if (distance > 0 && (!best || distance < best.distance)) {
404
- best = { candidate, distance };
405
- if (distance === 1)
406
- break;
402
+ if (Math.abs(candidate.length - cleanWord.length) > 2)
403
+ continue;
404
+ const distance = this.damerauLevenshtein(cleanWord, candidate);
405
+ if (distance === 0)
406
+ continue;
407
+ const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
408
+ const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
409
+ const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
410
+ if (better) {
411
+ best = { candidate, distance, firstMismatch, lengthMismatch };
407
412
  }
408
413
  }
409
414
  if (best && best.distance <= 2) {
@@ -482,24 +487,33 @@ var TDK = class {
482
487
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
483
488
  * hands back a single randomly-rotated rule per request (out of a pool of
484
489
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
485
- * draw would rarely match a given name — this re-draws (bounded, with a
486
- * short delay) until it finds a match or gives up. Every attempt bypasses
487
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
488
- * 25 attempts would just re-read the same cached `/icerik` response and
489
- * could never find a rule outside whatever the first draw happened to be.
490
- * Returns `null` if no match turns up within the attempt budget or the
491
- * matched page can't be parsed.
490
+ * draw would rarely match a given name — this re-draws until it finds a
491
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
492
+ * request is independent and stateless) rather than one-at-a-time with a
493
+ * delay: same total sample size (25) and hit probability as a sequential
494
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
495
+ * a miss resolves in roughly one round-trip time instead of several
496
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
497
+ * `enableCache(true)` is on, every attempt would just re-read the same
498
+ * cached `/icerik` response and could never find a rule outside whatever
499
+ * the first draw happened to be. Returns `null` if no match turns up
500
+ * within the attempt budget or the matched page can't be parsed.
492
501
  */
493
502
  static async getRule(name) {
494
503
  if (!name || name.trim() === "")
495
504
  return null;
496
505
  const target = name.trim().toLocaleLowerCase("tr-TR");
497
- for (let attempt = 0; attempt < 25; attempt++) {
498
- const rules = await this.getKurallar(true);
499
- const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
500
- if (match)
501
- return this.fetchRuleText(match.url);
502
- await this.delay(100);
506
+ const BATCH_SIZE = 5;
507
+ const ROUNDS = 5;
508
+ for (let round = 0; round < ROUNDS; round++) {
509
+ const batches = await Promise.all(
510
+ Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
511
+ );
512
+ for (const rules of batches) {
513
+ const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
514
+ if (match)
515
+ return this.fetchRuleText(match.url);
516
+ }
503
517
  }
504
518
  return null;
505
519
  }
@@ -647,6 +661,11 @@ var TDK = class {
647
661
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
648
662
  * returning each word's first meaning and etymological origin if found.
649
663
  * Looks each word up individually (throttled), so scales with text length.
664
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
665
+ * it does no morphological analysis, and neither does this method: a
666
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
667
+ * suffix) will come back `found: false` even though the root is a real
668
+ * headword. This is an inherent limitation of the data source, not a bug.
650
669
  */
651
670
  static async analyzeText(text) {
652
671
  const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
@@ -666,9 +685,13 @@ var TDK = class {
666
685
  return analyses;
667
686
  }
668
687
  /**
669
- * Classic edit-distance between two strings.
688
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
689
+ * like classic Levenshtein but also counts an adjacent-character
690
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
691
+ * two substitutions — a very common class of typo that plain Levenshtein
692
+ * otherwise misses.
670
693
  */
671
- static levenshtein(a, b) {
694
+ static damerauLevenshtein(a, b) {
672
695
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
673
696
  for (let i = 0; i <= a.length; i++)
674
697
  dp[i][0] = i;
@@ -678,6 +701,9 @@ var TDK = class {
678
701
  for (let j = 1; j <= b.length; j++) {
679
702
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
680
703
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
704
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
705
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
706
+ }
681
707
  }
682
708
  }
683
709
  return dp[a.length][b.length];
package/dist/cli.js CHANGED
@@ -424,11 +424,16 @@ var TDK = class {
424
424
  for (const candidate of this.autocompleteCache) {
425
425
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
426
426
  continue;
427
- const distance = this.levenshtein(cleanWord, candidate);
428
- if (distance > 0 && (!best || distance < best.distance)) {
429
- best = { candidate, distance };
430
- if (distance === 1)
431
- break;
427
+ if (Math.abs(candidate.length - cleanWord.length) > 2)
428
+ continue;
429
+ const distance = this.damerauLevenshtein(cleanWord, candidate);
430
+ if (distance === 0)
431
+ continue;
432
+ const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
433
+ const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
434
+ const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
435
+ if (better) {
436
+ best = { candidate, distance, firstMismatch, lengthMismatch };
432
437
  }
433
438
  }
434
439
  if (best && best.distance <= 2) {
@@ -507,24 +512,33 @@ var TDK = class {
507
512
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
508
513
  * hands back a single randomly-rotated rule per request (out of a pool of
509
514
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
510
- * draw would rarely match a given name — this re-draws (bounded, with a
511
- * short delay) until it finds a match or gives up. Every attempt bypasses
512
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
513
- * 25 attempts would just re-read the same cached `/icerik` response and
514
- * could never find a rule outside whatever the first draw happened to be.
515
- * Returns `null` if no match turns up within the attempt budget or the
516
- * matched page can't be parsed.
515
+ * draw would rarely match a given name — this re-draws until it finds a
516
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
517
+ * request is independent and stateless) rather than one-at-a-time with a
518
+ * delay: same total sample size (25) and hit probability as a sequential
519
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
520
+ * a miss resolves in roughly one round-trip time instead of several
521
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
522
+ * `enableCache(true)` is on, every attempt would just re-read the same
523
+ * cached `/icerik` response and could never find a rule outside whatever
524
+ * the first draw happened to be. Returns `null` if no match turns up
525
+ * within the attempt budget or the matched page can't be parsed.
517
526
  */
518
527
  static async getRule(name) {
519
528
  if (!name || name.trim() === "")
520
529
  return null;
521
530
  const target = name.trim().toLocaleLowerCase("tr-TR");
522
- for (let attempt = 0; attempt < 25; attempt++) {
523
- const rules = await this.getKurallar(true);
524
- const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
525
- if (match)
526
- return this.fetchRuleText(match.url);
527
- await this.delay(100);
531
+ const BATCH_SIZE = 5;
532
+ const ROUNDS = 5;
533
+ for (let round = 0; round < ROUNDS; round++) {
534
+ const batches = await Promise.all(
535
+ Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
536
+ );
537
+ for (const rules of batches) {
538
+ const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
539
+ if (match)
540
+ return this.fetchRuleText(match.url);
541
+ }
528
542
  }
529
543
  return null;
530
544
  }
@@ -672,6 +686,11 @@ var TDK = class {
672
686
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
673
687
  * returning each word's first meaning and etymological origin if found.
674
688
  * Looks each word up individually (throttled), so scales with text length.
689
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
690
+ * it does no morphological analysis, and neither does this method: a
691
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
692
+ * suffix) will come back `found: false` even though the root is a real
693
+ * headword. This is an inherent limitation of the data source, not a bug.
675
694
  */
676
695
  static async analyzeText(text) {
677
696
  const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
@@ -691,9 +710,13 @@ var TDK = class {
691
710
  return analyses;
692
711
  }
693
712
  /**
694
- * Classic edit-distance between two strings.
713
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
714
+ * like classic Levenshtein but also counts an adjacent-character
715
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
716
+ * two substitutions — a very common class of typo that plain Levenshtein
717
+ * otherwise misses.
695
718
  */
696
- static levenshtein(a, b) {
719
+ static damerauLevenshtein(a, b) {
697
720
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
698
721
  for (let i = 0; i <= a.length; i++)
699
722
  dp[i][0] = i;
@@ -703,6 +726,9 @@ var TDK = class {
703
726
  for (let j = 1; j <= b.length; j++) {
704
727
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
705
728
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
729
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
730
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
731
+ }
706
732
  }
707
733
  }
708
734
  return dp[a.length][b.length];
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TDK
4
- } from "./chunk-6BTOGV2M.mjs";
4
+ } from "./chunk-SNY3KUCF.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  var rawArgs = process.argv.slice(2);
package/dist/index.d.mts CHANGED
@@ -275,13 +275,17 @@ declare class TDK {
275
275
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
276
276
  * hands back a single randomly-rotated rule per request (out of a pool of
277
277
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
278
- * draw would rarely match a given name — this re-draws (bounded, with a
279
- * short delay) until it finds a match or gives up. Every attempt bypasses
280
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
281
- * 25 attempts would just re-read the same cached `/icerik` response and
282
- * could never find a rule outside whatever the first draw happened to be.
283
- * Returns `null` if no match turns up within the attempt budget or the
284
- * matched page can't be parsed.
278
+ * draw would rarely match a given name — this re-draws until it finds a
279
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
280
+ * request is independent and stateless) rather than one-at-a-time with a
281
+ * delay: same total sample size (25) and hit probability as a sequential
282
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
283
+ * a miss resolves in roughly one round-trip time instead of several
284
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
285
+ * `enableCache(true)` is on, every attempt would just re-read the same
286
+ * cached `/icerik` response and could never find a rule outside whatever
287
+ * the first draw happened to be. Returns `null` if no match turns up
288
+ * within the attempt budget or the matched page can't be parsed.
285
289
  */
286
290
  static getRule(name: string): Promise<string | null>;
287
291
  /**
@@ -314,12 +318,21 @@ declare class TDK {
314
318
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
315
319
  * returning each word's first meaning and etymological origin if found.
316
320
  * Looks each word up individually (throttled), so scales with text length.
321
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
322
+ * it does no morphological analysis, and neither does this method: a
323
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
324
+ * suffix) will come back `found: false` even though the root is a real
325
+ * headword. This is an inherent limitation of the data source, not a bug.
317
326
  */
318
327
  static analyzeText(text: string): Promise<WordAnalysis[]>;
319
328
  /**
320
- * Classic edit-distance between two strings.
329
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
330
+ * like classic Levenshtein but also counts an adjacent-character
331
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
332
+ * two substitutions — a very common class of typo that plain Levenshtein
333
+ * otherwise misses.
321
334
  */
322
- private static levenshtein;
335
+ private static damerauLevenshtein;
323
336
  /**
324
337
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
325
338
  */
package/dist/index.d.ts CHANGED
@@ -275,13 +275,17 @@ declare class TDK {
275
275
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
276
276
  * hands back a single randomly-rotated rule per request (out of a pool of
277
277
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
278
- * draw would rarely match a given name — this re-draws (bounded, with a
279
- * short delay) until it finds a match or gives up. Every attempt bypasses
280
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
281
- * 25 attempts would just re-read the same cached `/icerik` response and
282
- * could never find a rule outside whatever the first draw happened to be.
283
- * Returns `null` if no match turns up within the attempt budget or the
284
- * matched page can't be parsed.
278
+ * draw would rarely match a given name — this re-draws until it finds a
279
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
280
+ * request is independent and stateless) rather than one-at-a-time with a
281
+ * delay: same total sample size (25) and hit probability as a sequential
282
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
283
+ * a miss resolves in roughly one round-trip time instead of several
284
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
285
+ * `enableCache(true)` is on, every attempt would just re-read the same
286
+ * cached `/icerik` response and could never find a rule outside whatever
287
+ * the first draw happened to be. Returns `null` if no match turns up
288
+ * within the attempt budget or the matched page can't be parsed.
285
289
  */
286
290
  static getRule(name: string): Promise<string | null>;
287
291
  /**
@@ -314,12 +318,21 @@ declare class TDK {
314
318
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
315
319
  * returning each word's first meaning and etymological origin if found.
316
320
  * Looks each word up individually (throttled), so scales with text length.
321
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
322
+ * it does no morphological analysis, and neither does this method: a
323
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
324
+ * suffix) will come back `found: false` even though the root is a real
325
+ * headword. This is an inherent limitation of the data source, not a bug.
317
326
  */
318
327
  static analyzeText(text: string): Promise<WordAnalysis[]>;
319
328
  /**
320
- * Classic edit-distance between two strings.
329
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
330
+ * like classic Levenshtein but also counts an adjacent-character
331
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
332
+ * two substitutions — a very common class of typo that plain Levenshtein
333
+ * otherwise misses.
321
334
  */
322
- private static levenshtein;
335
+ private static damerauLevenshtein;
323
336
  /**
324
337
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
325
338
  */
package/dist/index.js CHANGED
@@ -438,11 +438,16 @@ var TDK = class {
438
438
  for (const candidate of this.autocompleteCache) {
439
439
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
440
440
  continue;
441
- const distance = this.levenshtein(cleanWord, candidate);
442
- if (distance > 0 && (!best || distance < best.distance)) {
443
- best = { candidate, distance };
444
- if (distance === 1)
445
- break;
441
+ if (Math.abs(candidate.length - cleanWord.length) > 2)
442
+ continue;
443
+ const distance = this.damerauLevenshtein(cleanWord, candidate);
444
+ if (distance === 0)
445
+ continue;
446
+ const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
447
+ const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
448
+ const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
449
+ if (better) {
450
+ best = { candidate, distance, firstMismatch, lengthMismatch };
446
451
  }
447
452
  }
448
453
  if (best && best.distance <= 2) {
@@ -521,24 +526,33 @@ var TDK = class {
521
526
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
522
527
  * hands back a single randomly-rotated rule per request (out of a pool of
523
528
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
524
- * draw would rarely match a given name — this re-draws (bounded, with a
525
- * short delay) until it finds a match or gives up. Every attempt bypasses
526
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
527
- * 25 attempts would just re-read the same cached `/icerik` response and
528
- * could never find a rule outside whatever the first draw happened to be.
529
- * Returns `null` if no match turns up within the attempt budget or the
530
- * matched page can't be parsed.
529
+ * draw would rarely match a given name — this re-draws until it finds a
530
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
531
+ * request is independent and stateless) rather than one-at-a-time with a
532
+ * delay: same total sample size (25) and hit probability as a sequential
533
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
534
+ * a miss resolves in roughly one round-trip time instead of several
535
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
536
+ * `enableCache(true)` is on, every attempt would just re-read the same
537
+ * cached `/icerik` response and could never find a rule outside whatever
538
+ * the first draw happened to be. Returns `null` if no match turns up
539
+ * within the attempt budget or the matched page can't be parsed.
531
540
  */
532
541
  static async getRule(name) {
533
542
  if (!name || name.trim() === "")
534
543
  return null;
535
544
  const target = name.trim().toLocaleLowerCase("tr-TR");
536
- for (let attempt = 0; attempt < 25; attempt++) {
537
- const rules = await this.getKurallar(true);
538
- const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
539
- if (match)
540
- return this.fetchRuleText(match.url);
541
- await this.delay(100);
545
+ const BATCH_SIZE = 5;
546
+ const ROUNDS = 5;
547
+ for (let round = 0; round < ROUNDS; round++) {
548
+ const batches = await Promise.all(
549
+ Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
550
+ );
551
+ for (const rules of batches) {
552
+ const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
553
+ if (match)
554
+ return this.fetchRuleText(match.url);
555
+ }
542
556
  }
543
557
  return null;
544
558
  }
@@ -686,6 +700,11 @@ var TDK = class {
686
700
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
687
701
  * returning each word's first meaning and etymological origin if found.
688
702
  * Looks each word up individually (throttled), so scales with text length.
703
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
704
+ * it does no morphological analysis, and neither does this method: a
705
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
706
+ * suffix) will come back `found: false` even though the root is a real
707
+ * headword. This is an inherent limitation of the data source, not a bug.
689
708
  */
690
709
  static async analyzeText(text) {
691
710
  const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
@@ -705,9 +724,13 @@ var TDK = class {
705
724
  return analyses;
706
725
  }
707
726
  /**
708
- * Classic edit-distance between two strings.
727
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
728
+ * like classic Levenshtein but also counts an adjacent-character
729
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
730
+ * two substitutions — a very common class of typo that plain Levenshtein
731
+ * otherwise misses.
709
732
  */
710
- static levenshtein(a, b) {
733
+ static damerauLevenshtein(a, b) {
711
734
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
712
735
  for (let i = 0; i <= a.length; i++)
713
736
  dp[i][0] = i;
@@ -717,6 +740,9 @@ var TDK = class {
717
740
  for (let j = 1; j <= b.length; j++) {
718
741
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
719
742
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
743
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
744
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
745
+ }
720
746
  }
721
747
  }
722
748
  return dp[a.length][b.length];
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  TDKError,
4
4
  TDKNetworkError,
5
5
  TDKValidationError
6
- } from "./chunk-6BTOGV2M.mjs";
6
+ } from "./chunk-SNY3KUCF.mjs";
7
7
  export {
8
8
  TDK,
9
9
  TDKError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdk-api-wrapper",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "TDK (Türk Dil Kurumu) unofficial live data API wrapper for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/tdk.ts CHANGED
@@ -409,17 +409,35 @@ export class TDK {
409
409
  // headword (by edit distance) across TDK's full ~81k-word list (the same
410
410
  // data `getSuggestions()` uses). Restricted to single-token, lowercase
411
411
  // headwords so it doesn't suggest compounds/phrases or proper nouns.
412
+ // Candidates whose length differs too much are skipped before running
413
+ // the O(n*m) distance calculation, both for speed and because a huge
414
+ // length gap can't be within the distance threshold anyway. Ties (same
415
+ // distance) prefer a matching first letter, then a matching length —
416
+ // typos rarely change the first letter, and this avoids picking
417
+ // whatever happens to sort alphabetically first. There's no word
418
+ // frequency data available, so a genuine tie can still land on a
419
+ // technically-correct but less commonly intended word.
412
420
  if (this.autocompleteCache.length === 0) {
413
421
  this.autocompleteCache = await this.fetchAutocompleteData();
414
422
  }
415
423
  const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
416
- let best: { candidate: string; distance: number } | null = null;
424
+ let best: { candidate: string; distance: number; firstMismatch: number; lengthMismatch: number } | null = null;
417
425
  for (const candidate of this.autocompleteCache) {
418
426
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
419
- const distance = this.levenshtein(cleanWord, candidate);
420
- if (distance > 0 && (!best || distance < best.distance)) {
421
- best = { candidate, distance };
422
- if (distance === 1) break;
427
+ if (Math.abs(candidate.length - cleanWord.length) > 2) continue;
428
+
429
+ const distance = this.damerauLevenshtein(cleanWord, candidate);
430
+ if (distance === 0) continue;
431
+
432
+ const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
433
+ const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
434
+ const better =
435
+ !best ||
436
+ distance < best.distance ||
437
+ (distance === best.distance && firstMismatch < best.firstMismatch) ||
438
+ (distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch);
439
+ if (better) {
440
+ best = { candidate, distance, firstMismatch, lengthMismatch };
423
441
  }
424
442
  }
425
443
  if (best && best.distance <= 2) {
@@ -502,23 +520,32 @@ export class TDK {
502
520
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
503
521
  * hands back a single randomly-rotated rule per request (out of a pool of
504
522
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
505
- * draw would rarely match a given name — this re-draws (bounded, with a
506
- * short delay) until it finds a match or gives up. Every attempt bypasses
507
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
508
- * 25 attempts would just re-read the same cached `/icerik` response and
509
- * could never find a rule outside whatever the first draw happened to be.
510
- * Returns `null` if no match turns up within the attempt budget or the
511
- * matched page can't be parsed.
523
+ * draw would rarely match a given name — this re-draws until it finds a
524
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
525
+ * request is independent and stateless) rather than one-at-a-time with a
526
+ * delay: same total sample size (25) and hit probability as a sequential
527
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
528
+ * a miss resolves in roughly one round-trip time instead of several
529
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
530
+ * `enableCache(true)` is on, every attempt would just re-read the same
531
+ * cached `/icerik` response and could never find a rule outside whatever
532
+ * the first draw happened to be. Returns `null` if no match turns up
533
+ * within the attempt budget or the matched page can't be parsed.
512
534
  */
513
535
  public static async getRule(name: string): Promise<string | null> {
514
536
  if (!name || name.trim() === "") return null;
515
537
  const target = name.trim().toLocaleLowerCase("tr-TR");
516
538
 
517
- for (let attempt = 0; attempt < 25; attempt++) {
518
- const rules = await this.getKurallar(true);
519
- const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
520
- if (match) return this.fetchRuleText(match.url);
521
- await this.delay(100);
539
+ const BATCH_SIZE = 5;
540
+ const ROUNDS = 5;
541
+ for (let round = 0; round < ROUNDS; round++) {
542
+ const batches = await Promise.all(
543
+ Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
544
+ );
545
+ for (const rules of batches) {
546
+ const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
547
+ if (match) return this.fetchRuleText(match.url);
548
+ }
522
549
  }
523
550
  return null;
524
551
  }
@@ -654,6 +681,11 @@ export class TDK {
654
681
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
655
682
  * returning each word's first meaning and etymological origin if found.
656
683
  * Looks each word up individually (throttled), so scales with text length.
684
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
685
+ * it does no morphological analysis, and neither does this method: a
686
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
687
+ * suffix) will come back `found: false` even though the root is a real
688
+ * headword. This is an inherent limitation of the data source, not a bug.
657
689
  */
658
690
  public static async analyzeText(text: string): Promise<WordAnalysis[]> {
659
691
  const words = text
@@ -679,9 +711,13 @@ export class TDK {
679
711
  }
680
712
 
681
713
  /**
682
- * Classic edit-distance between two strings.
714
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
715
+ * like classic Levenshtein but also counts an adjacent-character
716
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
717
+ * two substitutions — a very common class of typo that plain Levenshtein
718
+ * otherwise misses.
683
719
  */
684
- private static levenshtein(a: string, b: string): number {
720
+ private static damerauLevenshtein(a: string, b: string): number {
685
721
  const dp: number[][] = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
686
722
  for (let i = 0; i <= a.length; i++) dp[i][0] = i;
687
723
  for (let j = 0; j <= b.length; j++) dp[0][j] = j;
@@ -689,6 +725,9 @@ export class TDK {
689
725
  for (let j = 1; j <= b.length; j++) {
690
726
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
691
727
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
728
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
729
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
730
+ }
692
731
  }
693
732
  }
694
733
  return dp[a.length][b.length];