tdk-api-wrapper 1.5.1 → 1.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
@@ -90,7 +90,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak veya `TDKClient`
90
90
  - **`TDK.checkVowelHarmony(word)`**: Kelimenin büyük ünlü uyumuna uyup uymadığını (boolean) kontrol eder.
91
91
  - **`TDK.checkLabialHarmony(word)`**: Kelimenin küçük ünlü uyumuna (düzlük-yuvarlaklık uyumu) uyup uymadığını (boolean) kontrol eder.
92
92
  - **`TDK.getPartOfSpeech(word)`**: Kelimenin sözcük türünü (isim, sıfat, zarf vb.) döndürür.
93
- - **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'de doğrudan arar; bulamazsa TDK'nin "sık yapılan yanlışlar" listesini kontrol eder. Ardından **morfolojik ek sıyırma (stemming) motoru** devreye girer; kelime çekimli bir biçimse (`halılarımızın`, `kitabımız`, `çocuğa`, `okuyoruz`, `hakkımızda`, `başlıyor`) kökünü tespit edip `{ isCorrect: true, isInflected: true, root: "..." }` döner. Son aşamada ise TDK'nin ~81 bin kelimelik tam madde listesi üzerinde Damerau-Levenshtein edit-distance ile en yakın kelimeyi önerir (`yanlız` → `yalnız`).
93
+ - **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'de doğrudan arar; bulamazsa TDK'nin "sık yapılan yanlışlar" listesini kontrol eder. Ardından **morfolojik ek sıyırma (stemming) motoru** devreye girer; kelime çekimli bir biçimse (`halılarımızın`, `kitabımız`, `çocuğa`, `okuyoruz`, `hakkımızda`, `başlıyor`) kökünü tespit edip `{ isCorrect: true, isInflected: true, root: "..." }` döner. Son aşamada ise TDK'nin ~81 bin kelimelik tam madde listesi üzerinde **klavye ve düzeltme işareti farkındalıklı** bir Damerau-Levenshtein edit-distance ile en yakın kelimeyi önerir: Türkçe Q klavyede yan yana duran tuşlar (`arabs` → `araba`, `swlam` → `selam`) ve ASCII/Türkçe harf ikilileri (`ı/i`, `ş/s`, `ö/o` …) tam bir düzeltme yerine bir düzeltmenin küçük bir kesri kadar sayılır; böylece hem en yakın hem de en olası madde kazanır (`yanlız` → `yalnız`).
94
94
  - **`TDK.getCompoundWords(word)`**: Aranan kelime ile oluşturulmuş birleşik kelimeleri (Örn: dolma kalem) listeler.
95
95
 
96
96
  ### 3. Morfoloji ve Kök Bulma (Morphology Engine)
@@ -744,6 +744,57 @@ var COMMON_MISSPELLINGS = {
744
744
  babaanne: "babaanne"
745
745
  };
746
746
  var SEY_EXCEPTIONS = /* @__PURE__ */ new Set(["d\xFC\u015Fey", "e\u015Fey", "konsey", "jersey", "\u015Fey"]);
747
+ var KEYBOARD_ROWS = [
748
+ ["qwertyu\u0131op\u011F\xFC", 0],
749
+ ["asdfghjkl\u015Fi", 0.5],
750
+ ["zxcvbnm\xF6\xE7", 1]
751
+ ];
752
+ var KEYBOARD_COORDS = (() => {
753
+ const coords = {};
754
+ KEYBOARD_ROWS.forEach(([keys, offset], row) => {
755
+ [...keys].forEach((key, col) => {
756
+ coords[key] = [col + offset, row];
757
+ });
758
+ });
759
+ return coords;
760
+ })();
761
+ var DIACRITIC_SIBLINGS = {
762
+ \u0131: "i",
763
+ i: "\u0131",
764
+ \u00F6: "o",
765
+ o: "\xF6",
766
+ \u00FC: "u",
767
+ u: "\xFC",
768
+ \u015F: "s",
769
+ s: "\u015F",
770
+ \u00E7: "c",
771
+ c: "\xE7",
772
+ \u011F: "g",
773
+ g: "\u011F",
774
+ \u00E2: "a",
775
+ a: "\xE2"
776
+ };
777
+ var KEYBOARD_ROW_SUB_COST = 0.4;
778
+ var KEYBOARD_DIAGONAL_SUB_COST = 0.55;
779
+ var DIACRITIC_SUB_COST = 0.3;
780
+ var TRANSPOSITION_COST = 0.8;
781
+ function keyboardSubCost(a, b) {
782
+ if (a === b)
783
+ return 0;
784
+ if (DIACRITIC_SIBLINGS[a] === b)
785
+ return DIACRITIC_SUB_COST;
786
+ const pa = KEYBOARD_COORDS[a];
787
+ const pb = KEYBOARD_COORDS[b];
788
+ if (!pa || !pb)
789
+ return 1;
790
+ const dx = Math.abs(pa[0] - pb[0]);
791
+ const dy = Math.abs(pa[1] - pb[1]);
792
+ if (dy === 0 && dx <= 1 + 1e-9)
793
+ return KEYBOARD_ROW_SUB_COST;
794
+ if (dy === 1 && dx <= 1 + 1e-9)
795
+ return KEYBOARD_DIAGONAL_SUB_COST;
796
+ return 1;
797
+ }
747
798
  var TDK = class {
748
799
  static BASE_URL = "https://sozluk.gov.tr";
749
800
  static AUDIO_API_HOST = "api.sozluk.gov.tr";
@@ -1371,14 +1422,14 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1371
1422
  if (Math.abs(candidate.length - cleanWord.length) > 2)
1372
1423
  continue;
1373
1424
  const rawDist = this.damerauLevenshtein(cleanWord, candidate);
1374
- if (rawDist === 0)
1425
+ if (rawDist === 0 || rawDist > 2)
1375
1426
  continue;
1376
1427
  const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
1377
1428
  const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
1378
- const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
1379
- const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1429
+ const score = this.keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
1430
+ const better = !best || score < best.score - 1e-9 || Math.abs(score - best.score) < 1e-9 && firstMismatch < best.firstMismatch || Math.abs(score - best.score) < 1e-9 && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1380
1431
  if (better) {
1381
- best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
1432
+ best = { candidate, score, rawDist, firstMismatch, lengthMismatch };
1382
1433
  }
1383
1434
  }
1384
1435
  if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
@@ -1910,6 +1961,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1910
1961
  }
1911
1962
  return dp[a.length][b.length];
1912
1963
  }
1964
+ /**
1965
+ * Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
1966
+ * recurrence as {@link damerauLevenshtein}, but a substitution is charged by
1967
+ * {@link keyboardSubCost} (a fraction of an edit when the two letters are
1968
+ * adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
1969
+ * transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
1970
+ * still cost a full 1. Used only to *rank* spelling candidates; the plain
1971
+ * integer distance still gates whether a suggestion is offered at all.
1972
+ */
1973
+ static keyboardAwareDistance(a, b) {
1974
+ const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
1975
+ for (let i = 0; i <= a.length; i++)
1976
+ dp[i][0] = i;
1977
+ for (let j = 0; j <= b.length; j++)
1978
+ dp[0][j] = j;
1979
+ for (let i = 1; i <= a.length; i++) {
1980
+ for (let j = 1; j <= b.length; j++) {
1981
+ const cost = keyboardSubCost(a[i - 1], b[j - 1]);
1982
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
1983
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
1984
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + TRANSPOSITION_COST);
1985
+ }
1986
+ }
1987
+ }
1988
+ return dp[a.length][b.length];
1989
+ }
1913
1990
  /**
1914
1991
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
1915
1992
  */
package/dist/cli.js CHANGED
@@ -769,6 +769,57 @@ var COMMON_MISSPELLINGS = {
769
769
  babaanne: "babaanne"
770
770
  };
771
771
  var SEY_EXCEPTIONS = /* @__PURE__ */ new Set(["d\xFC\u015Fey", "e\u015Fey", "konsey", "jersey", "\u015Fey"]);
772
+ var KEYBOARD_ROWS = [
773
+ ["qwertyu\u0131op\u011F\xFC", 0],
774
+ ["asdfghjkl\u015Fi", 0.5],
775
+ ["zxcvbnm\xF6\xE7", 1]
776
+ ];
777
+ var KEYBOARD_COORDS = (() => {
778
+ const coords = {};
779
+ KEYBOARD_ROWS.forEach(([keys, offset], row) => {
780
+ [...keys].forEach((key, col) => {
781
+ coords[key] = [col + offset, row];
782
+ });
783
+ });
784
+ return coords;
785
+ })();
786
+ var DIACRITIC_SIBLINGS = {
787
+ \u0131: "i",
788
+ i: "\u0131",
789
+ \u00F6: "o",
790
+ o: "\xF6",
791
+ \u00FC: "u",
792
+ u: "\xFC",
793
+ \u015F: "s",
794
+ s: "\u015F",
795
+ \u00E7: "c",
796
+ c: "\xE7",
797
+ \u011F: "g",
798
+ g: "\u011F",
799
+ \u00E2: "a",
800
+ a: "\xE2"
801
+ };
802
+ var KEYBOARD_ROW_SUB_COST = 0.4;
803
+ var KEYBOARD_DIAGONAL_SUB_COST = 0.55;
804
+ var DIACRITIC_SUB_COST = 0.3;
805
+ var TRANSPOSITION_COST = 0.8;
806
+ function keyboardSubCost(a, b) {
807
+ if (a === b)
808
+ return 0;
809
+ if (DIACRITIC_SIBLINGS[a] === b)
810
+ return DIACRITIC_SUB_COST;
811
+ const pa = KEYBOARD_COORDS[a];
812
+ const pb = KEYBOARD_COORDS[b];
813
+ if (!pa || !pb)
814
+ return 1;
815
+ const dx = Math.abs(pa[0] - pb[0]);
816
+ const dy = Math.abs(pa[1] - pb[1]);
817
+ if (dy === 0 && dx <= 1 + 1e-9)
818
+ return KEYBOARD_ROW_SUB_COST;
819
+ if (dy === 1 && dx <= 1 + 1e-9)
820
+ return KEYBOARD_DIAGONAL_SUB_COST;
821
+ return 1;
822
+ }
772
823
  var TDK = class {
773
824
  static BASE_URL = "https://sozluk.gov.tr";
774
825
  static AUDIO_API_HOST = "api.sozluk.gov.tr";
@@ -1396,14 +1447,14 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1396
1447
  if (Math.abs(candidate.length - cleanWord.length) > 2)
1397
1448
  continue;
1398
1449
  const rawDist = this.damerauLevenshtein(cleanWord, candidate);
1399
- if (rawDist === 0)
1450
+ if (rawDist === 0 || rawDist > 2)
1400
1451
  continue;
1401
1452
  const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
1402
1453
  const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
1403
- const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
1404
- const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1454
+ const score = this.keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
1455
+ const better = !best || score < best.score - 1e-9 || Math.abs(score - best.score) < 1e-9 && firstMismatch < best.firstMismatch || Math.abs(score - best.score) < 1e-9 && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1405
1456
  if (better) {
1406
- best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
1457
+ best = { candidate, score, rawDist, firstMismatch, lengthMismatch };
1407
1458
  }
1408
1459
  }
1409
1460
  if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
@@ -1935,6 +1986,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1935
1986
  }
1936
1987
  return dp[a.length][b.length];
1937
1988
  }
1989
+ /**
1990
+ * Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
1991
+ * recurrence as {@link damerauLevenshtein}, but a substitution is charged by
1992
+ * {@link keyboardSubCost} (a fraction of an edit when the two letters are
1993
+ * adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
1994
+ * transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
1995
+ * still cost a full 1. Used only to *rank* spelling candidates; the plain
1996
+ * integer distance still gates whether a suggestion is offered at all.
1997
+ */
1998
+ static keyboardAwareDistance(a, b) {
1999
+ const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
2000
+ for (let i = 0; i <= a.length; i++)
2001
+ dp[i][0] = i;
2002
+ for (let j = 0; j <= b.length; j++)
2003
+ dp[0][j] = j;
2004
+ for (let i = 1; i <= a.length; i++) {
2005
+ for (let j = 1; j <= b.length; j++) {
2006
+ const cost = keyboardSubCost(a[i - 1], b[j - 1]);
2007
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
2008
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
2009
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + TRANSPOSITION_COST);
2010
+ }
2011
+ }
2012
+ }
2013
+ return dp[a.length][b.length];
2014
+ }
1938
2015
  /**
1939
2016
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
1940
2017
  */
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TDK
4
- } from "./chunk-NEC5MIQM.mjs";
4
+ } from "./chunk-ZFWZXNGY.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  var rawArgs = process.argv.slice(2);
package/dist/index.d.mts CHANGED
@@ -508,6 +508,16 @@ declare class TDK {
508
508
  * otherwise misses.
509
509
  */
510
510
  private static damerauLevenshtein;
511
+ /**
512
+ * Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
513
+ * recurrence as {@link damerauLevenshtein}, but a substitution is charged by
514
+ * {@link keyboardSubCost} (a fraction of an edit when the two letters are
515
+ * adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
516
+ * transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
517
+ * still cost a full 1. Used only to *rank* spelling candidates; the plain
518
+ * integer distance still gates whether a suggestion is offered at all.
519
+ */
520
+ private static keyboardAwareDistance;
511
521
  /**
512
522
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
513
523
  */
package/dist/index.d.ts CHANGED
@@ -508,6 +508,16 @@ declare class TDK {
508
508
  * otherwise misses.
509
509
  */
510
510
  private static damerauLevenshtein;
511
+ /**
512
+ * Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
513
+ * recurrence as {@link damerauLevenshtein}, but a substitution is charged by
514
+ * {@link keyboardSubCost} (a fraction of an edit when the two letters are
515
+ * adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
516
+ * transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
517
+ * still cost a full 1. Used only to *rank* spelling candidates; the plain
518
+ * integer distance still gates whether a suggestion is offered at all.
519
+ */
520
+ private static keyboardAwareDistance;
511
521
  /**
512
522
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
513
523
  */
package/dist/index.js CHANGED
@@ -793,6 +793,57 @@ var COMMON_MISSPELLINGS = {
793
793
  babaanne: "babaanne"
794
794
  };
795
795
  var SEY_EXCEPTIONS = /* @__PURE__ */ new Set(["d\xFC\u015Fey", "e\u015Fey", "konsey", "jersey", "\u015Fey"]);
796
+ var KEYBOARD_ROWS = [
797
+ ["qwertyu\u0131op\u011F\xFC", 0],
798
+ ["asdfghjkl\u015Fi", 0.5],
799
+ ["zxcvbnm\xF6\xE7", 1]
800
+ ];
801
+ var KEYBOARD_COORDS = (() => {
802
+ const coords = {};
803
+ KEYBOARD_ROWS.forEach(([keys, offset], row) => {
804
+ [...keys].forEach((key, col) => {
805
+ coords[key] = [col + offset, row];
806
+ });
807
+ });
808
+ return coords;
809
+ })();
810
+ var DIACRITIC_SIBLINGS = {
811
+ \u0131: "i",
812
+ i: "\u0131",
813
+ \u00F6: "o",
814
+ o: "\xF6",
815
+ \u00FC: "u",
816
+ u: "\xFC",
817
+ \u015F: "s",
818
+ s: "\u015F",
819
+ \u00E7: "c",
820
+ c: "\xE7",
821
+ \u011F: "g",
822
+ g: "\u011F",
823
+ \u00E2: "a",
824
+ a: "\xE2"
825
+ };
826
+ var KEYBOARD_ROW_SUB_COST = 0.4;
827
+ var KEYBOARD_DIAGONAL_SUB_COST = 0.55;
828
+ var DIACRITIC_SUB_COST = 0.3;
829
+ var TRANSPOSITION_COST = 0.8;
830
+ function keyboardSubCost(a, b) {
831
+ if (a === b)
832
+ return 0;
833
+ if (DIACRITIC_SIBLINGS[a] === b)
834
+ return DIACRITIC_SUB_COST;
835
+ const pa = KEYBOARD_COORDS[a];
836
+ const pb = KEYBOARD_COORDS[b];
837
+ if (!pa || !pb)
838
+ return 1;
839
+ const dx = Math.abs(pa[0] - pb[0]);
840
+ const dy = Math.abs(pa[1] - pb[1]);
841
+ if (dy === 0 && dx <= 1 + 1e-9)
842
+ return KEYBOARD_ROW_SUB_COST;
843
+ if (dy === 1 && dx <= 1 + 1e-9)
844
+ return KEYBOARD_DIAGONAL_SUB_COST;
845
+ return 1;
846
+ }
796
847
  var TDK = class {
797
848
  static BASE_URL = "https://sozluk.gov.tr";
798
849
  static AUDIO_API_HOST = "api.sozluk.gov.tr";
@@ -1420,14 +1471,14 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1420
1471
  if (Math.abs(candidate.length - cleanWord.length) > 2)
1421
1472
  continue;
1422
1473
  const rawDist = this.damerauLevenshtein(cleanWord, candidate);
1423
- if (rawDist === 0)
1474
+ if (rawDist === 0 || rawDist > 2)
1424
1475
  continue;
1425
1476
  const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
1426
1477
  const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
1427
- const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
1428
- const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1478
+ const score = this.keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
1479
+ const better = !best || score < best.score - 1e-9 || Math.abs(score - best.score) < 1e-9 && firstMismatch < best.firstMismatch || Math.abs(score - best.score) < 1e-9 && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1429
1480
  if (better) {
1430
- best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
1481
+ best = { candidate, score, rawDist, firstMismatch, lengthMismatch };
1431
1482
  }
1432
1483
  }
1433
1484
  if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
@@ -1959,6 +2010,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1959
2010
  }
1960
2011
  return dp[a.length][b.length];
1961
2012
  }
2013
+ /**
2014
+ * Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
2015
+ * recurrence as {@link damerauLevenshtein}, but a substitution is charged by
2016
+ * {@link keyboardSubCost} (a fraction of an edit when the two letters are
2017
+ * adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
2018
+ * transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
2019
+ * still cost a full 1. Used only to *rank* spelling candidates; the plain
2020
+ * integer distance still gates whether a suggestion is offered at all.
2021
+ */
2022
+ static keyboardAwareDistance(a, b) {
2023
+ const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
2024
+ for (let i = 0; i <= a.length; i++)
2025
+ dp[i][0] = i;
2026
+ for (let j = 0; j <= b.length; j++)
2027
+ dp[0][j] = j;
2028
+ for (let i = 1; i <= a.length; i++) {
2029
+ for (let j = 1; j <= b.length; j++) {
2030
+ const cost = keyboardSubCost(a[i - 1], b[j - 1]);
2031
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
2032
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
2033
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + TRANSPOSITION_COST);
2034
+ }
2035
+ }
2036
+ }
2037
+ return dp[a.length][b.length];
2038
+ }
1962
2039
  /**
1963
2040
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
1964
2041
  */
package/dist/index.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  restoreInfinitive,
14
14
  restoreVowelDrop,
15
15
  restoreVowelNarrowing
16
- } from "./chunk-NEC5MIQM.mjs";
16
+ } from "./chunk-ZFWZXNGY.mjs";
17
17
  export {
18
18
  TDK,
19
19
  TDKClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdk-api-wrapper",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
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
@@ -187,6 +187,67 @@ export const COMMON_MISSPELLINGS: Record<string, string> = {
187
187
 
188
188
  export const SEY_EXCEPTIONS = new Set(["düşey", "eşey", "konsey", "jersey", "şey"]);
189
189
 
190
+ /**
191
+ * Turkish Q (QWERTY) keyboard geometry for the spell checker's closest-headword
192
+ * fallback. Plain Damerau-Levenshtein treats every wrong letter as one full
193
+ * edit, so it cannot tell that "arabs" is much more likely a slip for "araba"
194
+ * (s and a sit next to each other) than for some equidistant headword, or that
195
+ * "swlam" is "selam" (w next to e). These tables let a substitution cost a
196
+ * fraction of an edit when the two keys are physically adjacent — or are the
197
+ * ASCII/diacritic pair of one another (ı/i, ş/s, ö/o, …), the other dominant
198
+ * class of Turkish typo — so the nearest *and* most plausible headword wins.
199
+ * Everyone is assumed to be on a Turkish Q layout.
200
+ */
201
+ const KEYBOARD_ROWS: ReadonlyArray<readonly [string, number]> = [
202
+ ["qwertyuıopğü", 0],
203
+ ["asdfghjklşi", 0.5],
204
+ ["zxcvbnmöç", 1],
205
+ ];
206
+ const KEYBOARD_COORDS: Readonly<Record<string, readonly [number, number]>> = (() => {
207
+ const coords: Record<string, readonly [number, number]> = {};
208
+ KEYBOARD_ROWS.forEach(([keys, offset], row) => {
209
+ [...keys].forEach((key, col) => {
210
+ coords[key] = [col + offset, row];
211
+ });
212
+ });
213
+ return coords;
214
+ })();
215
+
216
+ /** ASCII <-> Turkish-diacritic siblings, treated as an almost-free substitution. */
217
+ const DIACRITIC_SIBLINGS: Readonly<Record<string, string>> = {
218
+ ı: "i", i: "ı", ö: "o", o: "ö", ü: "u", u: "ü",
219
+ ş: "s", s: "ş", ç: "c", c: "ç", ğ: "g", g: "ğ", â: "a", a: "â",
220
+ };
221
+
222
+ /** Cost of substituting a key for its left/right neighbour on the same row. */
223
+ const KEYBOARD_ROW_SUB_COST = 0.4;
224
+ /** Cost of substituting a key for a diagonally adjacent one on the row above/below. */
225
+ const KEYBOARD_DIAGONAL_SUB_COST = 0.55;
226
+ /** Cost of confusing a letter with its diacritic/ASCII sibling. */
227
+ const DIACRITIC_SUB_COST = 0.3;
228
+ /** Cost of a transposition ("selam" <-> "selma"): a single wrong finger order. */
229
+ const TRANSPOSITION_COST = 0.8;
230
+
231
+ /**
232
+ * Weighted substitution cost between two single characters: 0 if identical,
233
+ * a small fraction if they are diacritic siblings or neighbouring keys on a
234
+ * Turkish Q keyboard, otherwise a full 1.
235
+ */
236
+ function keyboardSubCost(a: string, b: string): number {
237
+ if (a === b) return 0;
238
+ if (DIACRITIC_SIBLINGS[a] === b) return DIACRITIC_SUB_COST;
239
+ const pa = KEYBOARD_COORDS[a];
240
+ const pb = KEYBOARD_COORDS[b];
241
+ if (!pa || !pb) return 1;
242
+ const dx = Math.abs(pa[0] - pb[0]);
243
+ const dy = Math.abs(pa[1] - pb[1]);
244
+ // Same row, immediate horizontal neighbour: the most common slip.
245
+ if (dy === 0 && dx <= 1 + 1e-9) return KEYBOARD_ROW_SUB_COST;
246
+ // One row up/down and within roughly one key horizontally: a diagonal slip.
247
+ if (dy === 1 && dx <= 1 + 1e-9) return KEYBOARD_DIAGONAL_SUB_COST;
248
+ return 1;
249
+ }
250
+
190
251
  /**
191
252
  * TDK (Türk Dil Kurumu) API Wrapper
192
253
  */
@@ -865,27 +926,35 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
865
926
  }
866
927
 
867
928
  // 7. No exact match or morphology root: fall back to closest headword by edit distance.
868
- // Ties prefer matching first letter, and initial character mismatches are penalized
869
- // so irrelevant foreign loanwords (like 'jersey') do not beat Turkish roots.
870
- let best: { candidate: string; distance: number; rawDist: number; firstMismatch: number; lengthMismatch: number } | null = null;
929
+ // Candidates are ranked by the keyboard-/diacritic-aware distance (so "arabs" picks
930
+ // "araba" over an equidistant headword because s->a is a neighbouring-key slip, and
931
+ // "swlam" picks "selam" because w->e is), while the plain integer Damerau-Levenshtein
932
+ // still gates acceptance. Ties prefer matching first letter, then matching length, and
933
+ // initial character mismatches are penalized so irrelevant foreign loanwords (like
934
+ // 'jersey') do not beat Turkish roots.
935
+ let best:
936
+ | { candidate: string; score: number; rawDist: number; firstMismatch: number; lengthMismatch: number }
937
+ | null = null;
871
938
  for (const candidate of this.autocompleteCache) {
872
939
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
873
940
  if (Math.abs(candidate.length - cleanWord.length) > 2) continue;
874
941
 
875
942
  const rawDist = this.damerauLevenshtein(cleanWord, candidate);
876
- if (rawDist === 0) continue;
943
+ if (rawDist === 0 || rawDist > 2) continue;
877
944
 
878
945
  const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
879
946
  const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
880
- const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
947
+ const score = this.keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
881
948
 
882
949
  const better =
883
950
  !best ||
884
- distance < best.distance ||
885
- (distance === best.distance && firstMismatch < best.firstMismatch) ||
886
- (distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch);
951
+ score < best.score - 1e-9 ||
952
+ (Math.abs(score - best.score) < 1e-9 && firstMismatch < best.firstMismatch) ||
953
+ (Math.abs(score - best.score) < 1e-9 &&
954
+ firstMismatch === best.firstMismatch &&
955
+ lengthMismatch < best.lengthMismatch);
887
956
  if (better) {
888
- best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
957
+ best = { candidate, score, rawDist, firstMismatch, lengthMismatch };
889
958
  }
890
959
  }
891
960
  if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
@@ -1419,6 +1488,31 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1419
1488
  return dp[a.length][b.length];
1420
1489
  }
1421
1490
 
1491
+ /**
1492
+ * Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
1493
+ * recurrence as {@link damerauLevenshtein}, but a substitution is charged by
1494
+ * {@link keyboardSubCost} (a fraction of an edit when the two letters are
1495
+ * adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
1496
+ * transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
1497
+ * still cost a full 1. Used only to *rank* spelling candidates; the plain
1498
+ * integer distance still gates whether a suggestion is offered at all.
1499
+ */
1500
+ private static keyboardAwareDistance(a: string, b: string): number {
1501
+ const dp: number[][] = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
1502
+ for (let i = 0; i <= a.length; i++) dp[i][0] = i;
1503
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j;
1504
+ for (let i = 1; i <= a.length; i++) {
1505
+ for (let j = 1; j <= b.length; j++) {
1506
+ const cost = keyboardSubCost(a[i - 1], b[j - 1]);
1507
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
1508
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
1509
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + TRANSPOSITION_COST);
1510
+ }
1511
+ }
1512
+ }
1513
+ return dp[a.length][b.length];
1514
+ }
1515
+
1422
1516
  /**
1423
1517
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
1424
1518
  */
@@ -118,6 +118,17 @@ async function runTests() {
118
118
 
119
119
  const sp4 = await TDK.checkSpelling("asdfxyz12345");
120
120
  assert.strictEqual(sp4.isCorrect, false);
121
+
122
+ // Keyboard-aware ranking: "swlam" -> "selam" because w and e are horizontal
123
+ // neighbours on a Turkish Q keyboard (w->a is only a diagonal slip, so "salam" loses).
124
+ const sp5 = await TDK.checkSpelling("swlam");
125
+ assert.strictEqual(sp5.isCorrect, false);
126
+ assert.strictEqual(sp5.suggestion, "selam");
127
+
128
+ // Neighbouring-key substitution beats an equidistant headword: s and a are adjacent.
129
+ const sp6 = await TDK.checkSpelling("arabs");
130
+ assert.strictEqual(sp6.isCorrect, false);
131
+ assert.strictEqual(sp6.suggestion, "araba");
121
132
  console.log(" ✓ TDK.checkSpelling passed.");
122
133
 
123
134
  console.log("\n All morphology tests passed successfully!");