A multi-word exact anagram solver that combines exact letter matching with lexical frequency, WordNet grammar/valency, retained word-order candidates, positive bigram evidence, and optional Wikimedia phrase evidence.
The normal user-facing entry point is anagram_solver.py. The lower-level generator, reranker, corpus builder, and benchmark scripts remain available for research and debugging.
Python 3.13 or newer is recommended.
python anagram_solver.py "ODITIHNSLSHEEEPT"On the first run the solver may download/cache its dictionary, WordNet data, and word-frequency data. Runtime data lives under .anagram_data/ next to the scripts, so copying the project directory also carries its caches. The directory is git-ignored. Later runs reuse those caches.
Normal use runs a balanced search capped at 100,000 generated word bags. It is much more responsive than unlimited 2–6-word enumeration, but the cap means it can miss the answer if the correct bag occurs later in generation order.
For a faster exploratory pass:
python anagram_solver.py "ODITIHNSLSHEEEPT" --quick--quick caps generation at 20,000 candidate word bags and can miss the answer.
For unlimited candidate generation with no generation cap:
python anagram_solver.py "ODITIHNSLSHEEEPT" --exhaustive--exhaustive exhaustively generates matching word bags, but the user-facing reranker still deep-analyzes a bounded shortlist and only those deep-ranked rows are displayed. In other words, it removes generation truncation; it is not a promise that every generated bag receives full grammar/phrase analysis. It can become much slower when the word count is unknown or many short/common words fit the letter multiset. Supplying clues or an exact word count can reduce that search space dramatically.
If you know the answer contains four words:
python anagram_solver.py "ODITIHNSLSHEEEPT" --words 4Add clue words. At least one supplied hint must occur in the answer:
python anagram_solver.py "ODITIHNSLSHEEEPT" --hint dont --hint phoneExclude known-bad words:
python anagram_solver.py "ODITIHNSLSHEEEPT" --exclude hit,oldie,loisRequire a known word:
python anagram_solver.py "ODITIHNSLSHEEEPT" --require hipsShow more results per word-count bucket:
python anagram_solver.py "ODITIHNSLSHEEEPT" --top 25Admit rarer vocabulary by lowering the frequency cutoff:
python anagram_solver.py "tommarvoloriddle" --hint voldemort --words 4 --min-zipf 0The default --min-zipf 2.7 is a broad normal-English filter. Set it to 0 when rare names/terms matter, with the understanding that the search space can grow substantially.
For scripts or other programs, JSON output is available:
python anagram_solver.py "ODITIHNSLSHEEEPT" --words 4 --jsonUse --verbose to expose the underlying generator/reranker diagnostics.
The solver works without a phrase database and still uses positive-only observed bigrams. A Wikimedia phrase database can provide stronger late-stage evidence for known titles, names, sayings, and other attested phrases.
Build a Wiktionary index:
python build_wikimedia_phrase_index.py --rebuildOr include Wikipedia titles as well:
python build_wikimedia_phrase_index.py --include-wikipedia --rebuildThen use it while solving:
python anagram_solver.py "ODITIHNSLSHEEEPT" --phrase-db .anagram_data/phrase_indexes/wikimedia_phrases.dbThe combined Wikipedia build is large and intentionally not performed automatically by the normal solver.
The user-facing frontend stores intermediate candidate/reranked exports under:
.anagram_data/solver_runs/
All default generated/cache directories are children of .anagram_data/: solver runs, dictionaries, n-grams, WordNet, prepared rows, Wikimedia title downloads, phrase indexes, and benchmark artifacts. The cache key includes the generation constraints, generation mode/cap, and generator source hash, so repeating the same search can skip candidate generation while changed constraints/source code create a new cache entry. Use --rebuild to force regeneration or --work-root to choose a different location.
anagram_generate.py generates exact canonical word bags and applies lexical/clue filtering.
anagram_rerank.py consumes a candidates.txt export and performs WordNet grammar/valency analysis, retained top-K ordering, positive bigram scoring, and optional phrase-index rescoring.
anagram_benchmark.py runs the ordering or end-to-end benchmark suite.
build_wikimedia_phrase_index.py builds the optional SQLite phrase index.
For example, the low-level exhaustive path remains available:
python anagram_generate.py "ODITIHNSLSHEEEPT" --all-results --min-zipf 2.7 --export candidates.txt
python anagram_rerank.py candidates.txt --export reranked.txtpython -m unittest discover -s tests -vAll shared real-world test/CI cases live in one case-centric registry: anagram_benchmarks.json. The Python API in anagram_suite.py exposes cases_for(...), and the scenario-driven CI runners select their workloads from it:
ordering— blocking ordering regression benchmark;phrase_ordering— phrase/corpus ordering A/B;normal_user_cli— realanagram_solver.pyinvocations;full— full generation + reranking matrix;performance— repeatable ordering/deep-analysis workload;refinement— forced-beam k-opt experiment;feature_ranker— grouped ranker training/evaluation.
A case with no suites field defaults to all suites. Existing cases use explicit core, full, performance, or CLI memberships where needed to preserve the intended runtime. core expands to ordering, phrase ordering, refinement, and feature-ranker evaluation; all expands to every suite.
A case can own common solver options plus suite-specific overrides. For example:
{
"id": "my_new_anagram",
"target": "IAMTESTINGANAGRAMS",
"answer": "i am testing anagrams",
"solver": {
"hints": ["testing"],
"words": 4,
"min_zipf": 2.7
},
"normal_user_cli": {
"verbose": true,
"timeout_seconds": 120
},
"ordering": {
"max_rank": 10
}
}Because suites is omitted, that single object participates in every registry-backed CI suite. Add "suites": ["core", "normal_user_cli"] (or any explicit suite list) to restrict it. Set "enabled": false to temporarily disable a case without deleting it.
There is also a small management CLI, so routine edits do not require hand-editing JSON:
python anagram_suite.py list
python anagram_suite.py list --suite normal_user_cli
python anagram_suite.py add my_new_anagram --target IAMTESTINGANAGRAMS --answer "i am testing anagrams" --hint testing --words 4 --verbose
python anagram_suite.py disable my_new_anagram
python anagram_suite.py enable my_new_anagram
python anagram_suite.py remove my_new_anagram
python anagram_suite.py validateadd defaults to all suites. Use repeated --suite flags to restrict a new case, for example --suite core --suite normal_user_cli.
Small synthetic fixtures that exist only to exercise one function or subsystem should still stay beside that subsystem's unit tests. Test modules are grouped by behavior rather than by the PR/review that introduced a regression.
Pull requests also run the fast phrase-order A/B benchmark. The expensive full corpus matrix is kept as an explicit manual workflow rather than running after every merge.