mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(authors): keep non-latin letters when matching enrichment candidates (#1615)
* fix(authors): keep non-latin letters when matching enrichment candidates Keep letters and digits of any script in normalizeName() instead of dropping everything outside [a-z0-9], and stop treating two empty token lists as a token-set match. Names without ASCII letters normalized to an empty string, so any two of them scored 0.96 and passed the confidence gate, and enrichment linked authors to unrelated people. Closes #1459 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(authors): keep combining marks when normalizing author names Keep \p{M} in normalizeName() alongside letters and digits. Vowel signs in scripts such as Devanagari are combining marks, not letters, so dropping them merged distinct names: कमल and कमला both normalized to कमल and matched with score 1. Latin diacritics are still folded by the earlier U+0300-U+036F strip, so Latin and Cyrillic matching is unchanged. Refs #1459 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
This commit is contained in:
@@ -57,4 +57,42 @@ describe('author-name-match', () => {
|
||||
]);
|
||||
expect(best).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects an unrelated candidate when both names have no ASCII letters', () => {
|
||||
const match = scoreAuthorNameMatch('Агата Кристи', candidate('Андрея Илиев'));
|
||||
expect(match.tokenSetEqual).toBe(false);
|
||||
expect(match.accept).toBe(false);
|
||||
});
|
||||
|
||||
it('returns null when every non-ASCII candidate is unrelated', () => {
|
||||
const best = pickBestAuthorNameMatch('Агата Кристи', [candidate('Андрея Илиев'), candidate('Борис Акунин')]);
|
||||
expect(best).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects same-last-name false positives in a non-Latin script', () => {
|
||||
const match = scoreAuthorNameMatch('Агата Кристи', candidate('Иван Кристи'));
|
||||
expect(match.lastTokenExact).toBe(true);
|
||||
expect(match.accept).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts matching names in a non-Latin script', () => {
|
||||
const exact = scoreAuthorNameMatch('Агата Кристи', candidate('Агата Кристи'));
|
||||
const swapped = scoreAuthorNameMatch('Кристи Агата', candidate('Агата Кристи'));
|
||||
expect(exact.accept).toBe(true);
|
||||
expect(exact.score).toBe(1);
|
||||
expect(swapped.accept).toBe(true);
|
||||
expect(swapped.tokenSetEqual).toBe(true);
|
||||
});
|
||||
|
||||
it('never treats two names without letters or digits as a token-set match', () => {
|
||||
const match = scoreAuthorNameMatch('...', candidate('?!'));
|
||||
expect(match.tokenSetEqual).toBe(false);
|
||||
expect(match.accept).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps script-specific combining marks, so names that differ only by one stay distinct', () => {
|
||||
const match = scoreAuthorNameMatch('कमल', candidate('कमला'));
|
||||
expect(match.exactNormalized).toBe(false);
|
||||
expect(match.accept).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ function normalizeName(value: string): string {
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]/g, ' ')
|
||||
.replace(/[^\p{L}\p{M}\p{N}\s]/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
@@ -95,6 +95,7 @@ function overlapRatio(queryTokens: string[], candidateTokens: string[]): number
|
||||
}
|
||||
|
||||
function haveEqualTokenMultiset(a: string[], b: string[]): boolean {
|
||||
if (a.length === 0 || b.length === 0) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
const aSorted = [...a].sort();
|
||||
const bSorted = [...b].sort();
|
||||
|
||||
Reference in New Issue
Block a user