jellyfish soundex, metaphone and Jaro-Winkler for name matching: working versions (tested 2026-10-16)
Build a case and accent insensitive name matcher from jellyfish phonetic keys and Jaro-Winkler similarity, handling soundex collisions, spelling variants and empty keys.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
jellyfish@1.3.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("jellyfish-phonetic-match") (install), or see them on GitHub.
requirements.txt
jellyfish==1.3.0 pytest==9.1.1
matcher.py
import unicodedata
import jellyfish
def normalize(name):
# NFKD + ascii drop makes matching accent and case insensitive
ascii_name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
return " ".join(ascii_name.upper().split())
def soundex_key(name):
# soundex is a fixed 4-char code, so unrelated surnames often collide
return jellyfish.soundex(normalize(name))
def metaphone_key(name):
# metaphone is variable length and collapses non-letters to an empty string
return jellyfish.metaphone(normalize(name))
def similarity(a, b):
return jellyfish.jaro_winkler_similarity(normalize(a), normalize(b))
def is_match(a, b, threshold=0.9):
na, nb = normalize(a), normalize(b)
if not na or not nb:
return False
if na == nb:
return True
ma, mb = metaphone_key(na), metaphone_key(nb)
if ma and ma == mb:
return True
return similarity(na, nb) >= threshold
def best_match(query, candidates):
scored = ((c, similarity(query, c)) for c in candidates)
choice, score = max(scored, key=lambda pair: pair[1], default=(None, 0.0))
return {"choice": choice, "score": score}
tests/test_matcher.py
import jellyfish
import pytest
from matcher import best_match, is_match, metaphone_key, normalize, similarity, soundex_key
def test_jaro_winkler_needs_normalized_input():
# jellyfish does no cleaning: a raw case mismatch scores zero, accents drag it down
assert jellyfish.jaro_winkler_similarity("smith", "SMITH") == 0.0
assert normalize(" Jose\u0301 ") == "JOSE"
assert similarity("smith", "SMITH") == 1.0
assert similarity("José", "Jose") == 1.0
def test_prefix_boost_on_transposition():
# Jaro-Winkler adds a bonus for the shared MAR- prefix
assert round(similarity("MARTHA", "MARHTA"), 3) == 0.961
def test_soundex_collides_but_matcher_rejects():
# both names share soundex R163, a classic false positive
assert soundex_key("Robert") == soundex_key("Rupert") == "R163"
assert not is_match("Robert", "Rupert")
def test_metaphone_bridges_spelling_variants():
assert metaphone_key("Catherine") == metaphone_key("Katherine") == "K0RN"
assert is_match("Catherine", "Katherine")
def test_empty_phonetic_keys_are_guarded():
# digits and non-Latin names reduce to an empty key, so guard against it
assert metaphone_key("123") == ""
assert not is_match("", "")
assert not is_match("\u674e", "")
def test_best_match_picks_closest_candidate():
result = best_match("Jon", ["Jane", "John", "Jason"])
assert result["choice"] == "John"
assert result["score"] == pytest.approx(0.933, abs=1e-3)
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.