Soundex in JavaScript: phonetic matching for names and misspellings

Edit distance asks how many keystrokes separate two spellings. Phonetic matching asks a different question: would these two words sound the same if you read them aloud? Soundex, the oldest and simplest answer, turns a name into a four-character code so that Smith and Smyth land in the same drawer. It is crude, it is more than a century old, and it is still built into databases, which makes it worth understanding properly.
Where Soundex comes from
Robert C. Russell filed a patent for a phonetic "index" in October 1917; it was granted in April 1918 as US patent 1,261,167. The idea was to file names by the way they sound rather than the way a clerk happened to spell them. The system became best known through the United States census: the National Archives still publishes the coding rules, because its Soundex indexes to several censuses are filed by these codes and genealogists need to compute them to find a family.
That origin explains its design. It was meant to be applied by hand, by people working through surname cards, so every rule had to be simple enough to remember. It was never meant to model pronunciation in detail, and it assumes English-language spelling habits throughout.
The rules, as the National Archives states them
A Soundex code is one letter followed by three digits, such as W252 for Washington. The letter is the first letter of the name. The digits encode the consonants that follow:
| Digit | Letters |
|---|---|
| 1 | B, F, P, V |
| 2 | C, G, J, K, Q, S, X, Z |
| 3 | D, T |
| 4 | L |
| 5 | M, N |
| 6 | R |
A, E, I, O, U, H, W and Y get no digit. Then come the details that most quick implementations get wrong:
- Stop at three digits, pad with zeros. Lee becomes L000; anything after the third digit is ignored.
- Double letters count once. Gutierrez is G362: the second R adds nothing.
- Neighbours with the same digit count once, including the first letter. Pfister is P236, not P123, because F has the same digit as the P before it. Jackson is J250: C, K and S all share digit 2.
- A vowel resets the run. In Tymczak (T522), Z is absorbed by the C before it, but the A between Z and K separates them, so K is coded again.
- H and W do not reset it. Ashcraft is A261, not A226: the H between S and C does not separate them, so C is absorbed.
The guide adds a rule for people rather than programs: names with prefixes such as Van, De or La should be coded both with and without the prefix, because a clerk may have filed them either way (VanDeusen gives V532 or D250). Mc and Mac are not treated as prefixes.
A JavaScript implementation
The function below follows those rules. It tracks the digit of the last coded consonant in prev; a vowel (or Y) clears it, while H and W leave it alone.
const CODES = {
b: 1, f: 1, p: 1, v: 1,
c: 2, g: 2, j: 2, k: 2, q: 2, s: 2, x: 2, z: 2,
d: 3, t: 3, l: 4, m: 5, n: 5, r: 6
};
function soundex(name) {
const s = name.toLowerCase().replace(/[^a-z]/g, '');
if (!s) return '';
let code = s[0].toUpperCase();
let prev = CODES[s[0]] || 0; // the first letter blocks a twin
for (let i = 1; i < s.length && code.length < 4; i++) {
const digit = CODES[s[i]];
if (digit) {
if (digit !== prev) code += digit;
prev = digit;
} else if (s[i] !== 'h' && s[i] !== 'w') {
prev = 0; // a vowel separates equal digits
}
}
return code.padEnd(4, '0');
}
soundex('Washington'); // "W252"
soundex('Pfister'); // "P236"
soundex('Tymczak'); // "T522"
soundex('Ashcraft'); // "A261"
All seven worked examples in the National Archives guide (Washington, Lee, Gutierrez, Pfister, Jackson, Tymczak, Ashcraft) come out as published. One judgment call remains: the guide lists Y among the ignored letters but only names A, E, I, O and U as separators. Treating Y as a separator, as here, is the common choice, and it only changes the result in rare names where Y sits between two consonants with the same digit.
Note that the regular expression drops anything outside a to z. For a name with accents, normalize first and strip the combining marks (name.normalize('NFD').replace(/[̀-ͯ]/g, '')) or you will lose letters. Apostrophes and hyphens disappear, which is usually what you want for O'Brien or Smith-Jones.
Using it: a phonetic index
Soundex is most useful as a blocking key: a cheap way to narrow a large list to a handful of plausible candidates, which you then rank with something finer. Build a map from code to words once, then look up the code of each query:
function buildIndex(words) {
const index = new Map();
for (const w of words) {
const key = soundex(w);
if (!index.has(key)) index.set(key, []);
index.get(key).push(w);
}
return index;
}
function soundsLike(query, index, limit = 5) {
const q = query.toLowerCase();
return (index.get(soundex(query)) || [])
.map(w => [w, levenshtein(q, w.toLowerCase())])
.sort((x, y) => x[1] - y[1] || x[0].localeCompare(y[0]))
.slice(0, limit)
.map(([w]) => w);
}
const names = buildIndex(['Smith', 'Smyth', 'Schmidt', 'Snead',
'Smoot', 'Stewart', 'Stuart', 'Steward']);
soundsLike('Smithe', names); // ["Smith", "Smyth", "Smoot", "Schmidt", "Snead"]
soundsLike('Stewert', names); // ["Stewart", "Steward", "Stuart"]
The levenshtein function is the one from our step-by-step guide to edit distance. The output shows both halves of the bargain: the lookup is a single map access however long the list, and the ranking puts the sensible answers first. It also shows the noise. Snead and Smoot share the code S530 with Smith, which nobody would confuse aloud.
Where Soundex fails
- The first letter is never encoded. Kathy is K300 and Cathy is C300; fonetik is F532 and phonetic is P532. Any error in the first letter makes two names look unrelated. A common workaround is to index each name under its code and under the code of a few first-letter variants (C/K, F/Ph, and so on).
- Letters are not sounds. Soundex sees the letters, so the silent K in Knight is coded (K523) and the C of Cecil is coded like a K. The digits describe spelling groups that happen to sound alike in many English names, nothing more.
- Too few codes. Only the first consonants count, so long names that start alike collide, and short names that differ late do too: Robert and Rupert are both R163.
- English assumptions. PostgreSQL's documentation says it plainly: Soundex "is not very useful for non-English names".
Better phonetic codes
Daitch-Mokotoff Soundex was designed for Slavic, Germanic and Yiddish surnames. As the PostgreSQL documentation summarises, it codes the first six meaningful letters rather than four, treats letter combinations as single sounds, and returns several codes when a spelling has more than one plausible pronunciation: PostgreSQL gives John two codes and Schwartzenegger one. Matching then means "do the two lists of codes overlap?".
Metaphone, published by Lawrence Philips in 1990, and its successor Double Metaphone work from English pronunciation rules rather than letter groups: PH sounds like F, a K before N is silent, and so on. Double Metaphone returns a primary and an alternate key to cover names whose pronunciation is ambiguous. These codes catch many of the cases above, including the first-letter problem, at the cost of a much longer rule set.
You rarely need to write these yourself. PostgreSQL's fuzzystrmatch extension offers soundex(), difference() (how many of the four characters match, so difference('Anne', 'Andrew') is 2), daitch_mokotoff(), metaphone() and dmetaphone(), with a warning that the Soundex and Metaphone functions do not handle multibyte encodings such as UTF-8 well. MySQL has a SOUNDEX() function too, but it does not stop at four characters, so compare it with care against codes from other systems.
Phonetic codes in spell checking
Most misspellings in running text are typing slips, and edit distance handles them well. Phonetic codes help with the other kind: words spelled the way they sound, by someone who does not know the conventional spelling. That is why several spell checkers combine the two signals, as our tour of how spell checkers work describes. The Letterbench checker, documented in its notes on suggestion ranking, relies on edits rather than sounds.
Phonetic matching cannot solve the opposite problem. Words that sound the same but are spelled correctly, such as their and there, are exactly what a sound-based code merges, so it can never flag them; see homophones and real-word errors for why that needs context rather than a better dictionary.
Use Soundex to find candidates, never to decide: the code narrows the list, and something finer has to choose.
Sources
- National Archives (US), The Soundex Indexing System.
- PostgreSQL documentation, fuzzystrmatch: determine string similarities and distance.
- Robert C. Russell, Index, US patent 1,261,167, granted April 2, 1918.
Under the hoodLevenshtein distance in JavaScript
Edit distance step by step, with the Damerau transposition variant.
Under the hoodHow spell checkers work
Word lists, affix rules, edit distance and how suggestions get ranked.
Spelling & writingHomophones spell checkers miss
Their, there, they're: real-word errors no dictionary lookup can catch.