You could also insert a threshold for successful word recognition in the Python script from the outset. For example, a candidate word may only deviate from the form of the Voynich word to such an extent that no more than about one third of the letters are different (MAX_RATIO = 0.35). However, this would result in very few contiguous hits. Our solution finder is unlikely to look at the text with such "precision". Where is the leeway in that ? 
Total words: 38529
Matches: 11348 (29.45%)
No matches: 27181 (70.55%)

Code:
#!/usr/bin/env python3
# coding: utf-8
"""
DoubleMetaphone + Levenshtein Hybrid Decoder (with MAX_RATIO-threshold)
-----------------------------------------------------------------------
1. Load Latin words from wordlist.txt
2. Build a DoubleMetaphone index
3. For each Voynich word:
- Compute its DoubleMetaphone codes
- Find matching or phonetically similar Latin words
- Rank candidates using Levenshtein distance
- Output the best candidate or <no match>
"""
import sys
from metaphone import doublemetaphone
from collections import defaultdict
from tqdm import tqdm
# ---------------------------------------------------------
# Parameter: maximum allowed relative distance
# ---------------------------------------------------------
MAX_RATIO = 0.35 # e.g., 35% of word length
# ---------------------------------------------------------
# Levenshtein distance
# ---------------------------------------------------------
def levenshtein(a, b):
if not a:
return len(b)
if not b:
return len(a)
dp = range(len(b) + 1)
for i, ca in enumerate(a, 1):
new_dp = [i]
for j, cb in enumerate(b, 1):
if ca == cb:
new_dp.append(dp[j-1])
else:
new_dp.append(1 + min(dp[j-1], dp[j], new_dp[-1]))
dp = new_dp
return dp[-1]
# ---------------------------------------------------------
# Load Latin words
# ---------------------------------------------------------
def load_wordlist(path="wordlist.txt"):
words = []
with open(path, "r", encoding="utf8") as f:
for line in f:
word = line.strip().lower()
if word:
words.append(word)
return words
# ---------------------------------------------------------
# Normalize a Voynich word
# ---------------------------------------------------------
def normalize_voynich_word(w):
return "".join(c.lower() for c in w if c.isalpha())
# ---------------------------------------------------------
# Build Metaphone index
# ---------------------------------------------------------
def build_metaphone_index(words):
index = defaultdict(list)
for w in words:
m1, m2 = doublemetaphone(w)
if m1:
index[m1].append(w)
if m2 and m2 != m1:
index[m2].append(w)
return index
# ---------------------------------------------------------
# Combined DoubleMetaphone + Levenshtein matching
# ---------------------------------------------------------
def hybrid_match(v_word, index, top_n=1):
if not v_word:
return ["<no match>"]
m1, m2 = doublemetaphone(v_word)
candidates = []
# 1) Exact metaphone matches
if m1 in index:
candidates.extend(index[m1])
if m2 in index:
candidates.extend(index[m2])
# 2) If too few candidates, expand by prefix similarity
if len(candidates) < 5 and m1:
prefix = m1[:2]
for key in index:
if key.startswith(prefix):
candidates.extend(index[key])
if not candidates:
return ["<no match>"]
# 3) Compute Levenshtein distances
scored = []
for cand in candidates:
# compare only equal-length segment
score = levenshtein(v_word, cand[:len(v_word)])
scored.append((score, cand))
scored.sort(key=lambda x: x[0])
best_score = scored[0][0]
word_len = len(v_word)
# 4) Check acceptance threshold
max_allowed = max(1, int(word_len * MAX_RATIO))
if best_score > max_allowed:
return ["<no match>"]
# 5) All candidates with the same best score distance
best_candidates = [w for s, w in scored if s == best_score]
return best_candidates[:top_n]
# ---------------------------------------------------------
# MAIN
# ---------------------------------------------------------
def main():
if len(sys.argv) < 3:
print("Usage: python3 phonetic_levenshtein.py voyn2latin.txt output.txt")
sys.exit(1)
infile = sys.argv[1]
outfile = sys.argv[2]
print("Loading Latin wordlist …")
latin_words = load_wordlist("wordlist.txt")
print(f"{len(latin_words)} Latin words loaded.")
print("Building DoubleMetaphone index …")
index = build_metaphone_index(latin_words)
print(f"Index contains {len(index)} metaphone keys.")
print("Decoding …")
with open(infile, "r", encoding="utf8") as f:
lines = f.readlines()
output_lines = []
for line in tqdm(lines, desc="Lines"):
if not line.strip():
output_lines.append("")
continue
words = line.split()
decoded_line = []
for w in words:
normalized = normalize_voynich_word(w)
matches = hybrid_match(normalized, index, top_n=1)
decoded_line.append(matches[0])
output_lines.append(" ".join(decoded_line))
with open(outfile, "w", encoding="utf8") as f:
for row in output_lines:
f.write(row + "\n")
print("Done →", outfile)
if __name__ == "__main__":
main()Total words: 38529
Matches: 11348 (29.45%)
No matches: 27181 (70.55%)

