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 +1 -1
- package/dist/{chunk-NEC5MIQM.mjs → chunk-ZFWZXNGY.mjs} +81 -4
- package/dist/cli.js +81 -4
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +81 -4
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/tdk.ts +103 -9
- package/test/morphology.test.js +11 -0
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
|
|
1379
|
-
const better = !best ||
|
|
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,
|
|
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
|
|
1404
|
-
const better = !best ||
|
|
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,
|
|
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
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
|
|
1428
|
-
const better = !best ||
|
|
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,
|
|
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
package/package.json
CHANGED
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
|
-
//
|
|
869
|
-
//
|
|
870
|
-
|
|
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
|
|
947
|
+
const score = this.keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
|
|
881
948
|
|
|
882
949
|
const better =
|
|
883
950
|
!best ||
|
|
884
|
-
|
|
885
|
-
(
|
|
886
|
-
(
|
|
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,
|
|
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
|
*/
|
package/test/morphology.test.js
CHANGED
|
@@ -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!");
|