{"id":51658624,"url":"https://github.com/iliaal/phonetic","last_synced_at":"2026-07-14T13:03:03.491Z","repository":{"id":368305681,"uuid":"1284503018","full_name":"iliaal/phonetic","owner":"iliaal","description":"Native PHP phonetic matching: Double Metaphone, Beider-Morse (BMPM), Daitch-Mokotoff Soundex, NYSIIS, and Match Rating Approach.","archived":false,"fork":false,"pushed_at":"2026-07-12T18:06:04.000Z","size":724,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-07-12T20:05:39.926Z","etag":null,"topics":["beider-morse","bmpm","daitch-mokotoff","double-metaphone","fuzzy-matching","match-rating","metaphone","nysiis","phonetic","php","php-extension","pie","soundex"],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iliaal.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-29T23:48:32.000Z","updated_at":"2026-07-12T18:07:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/iliaal/phonetic","commit_stats":null,"previous_names":["iliaal/phonetic"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/iliaal/phonetic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iliaal%2Fphonetic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iliaal%2Fphonetic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iliaal%2Fphonetic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iliaal%2Fphonetic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iliaal","download_url":"https://codeload.github.com/iliaal/phonetic/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iliaal%2Fphonetic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35462449,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-14T02:00:06.603Z","response_time":114,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["beider-morse","bmpm","daitch-mokotoff","double-metaphone","fuzzy-matching","match-rating","metaphone","nysiis","phonetic","php","php-extension","pie","soundex"],"created_at":"2026-07-14T13:03:02.648Z","updated_at":"2026-07-14T13:03:03.482Z","avatar_url":"https://github.com/iliaal.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# phonetic\n\nNative phonetic matching for PHP: **Double Metaphone**, **Beider-Morse Phonetic Matching (BMPM)**, **Daitch-Mokotoff Soundex**, **NYSIIS**, and **Match Rating Approach**, the phonetic name-matching encoders that PHP core does not ship. It also ships comparison helpers that answer \"do these two names sound alike?\" directly.\n\nPHP core has `soundex()` and `metaphone()`, but not these, which are the standard tools for fuzzy name matching, record linkage, and genealogy search across spelling and transliteration variants.\n\n![phonetic](images/phonetic-hero.jpg)\n\n## Quick Start\n\nInstall via [PIE](https://github.com/php/pie) (requires PHP 8.1 or later):\n\n```sh\npie install iliaal/phonetic\n```\n\nThen ask whether two names sound alike, no userland matching logic required:\n\n```php\ndouble_metaphone_match(\"Catherine\", \"Kathryn\");   // 2  (strong match)\ndm_soundex_match(\"Moskowitz\", \"Moskovitz\");        // true\nbmpm_match(\"Peterson\", \"Petersen\");                // true\n```\n\n## Choosing an algorithm\n\n| | Double Metaphone | BMPM | Daitch-Mokotoff Soundex | NYSIIS | Match Rating |\n|---|---|---|---|---|---|\n| Output | primary + alternate key | language-aware token set | distinct 6-digit codes | single key | compact codex |\n| Two names match when | keys are equal | token sets intersect | code sets intersect | keys are equal | clear the MRA similarity threshold |\n| Strongest for | English and general Latin-script names | cross-language and transliteration variants (Slavic, Germanic, Hebrew, Romance) | Eastern-European and Ashkenazi surnames, genealogy | American/English surnames | English names; ships its own similarity test |\n| Spelling-variant recall | good | highest | high, within its language model | good | good |\n| Ambiguity handling | up to 2 keys | many tokens | multiple codes | single key | single codex |\n| Relative speed | fast (1.0x) | slowest (~60x) | middle (~2.3x) | fast (0.42x) | fastest (0.24x) |\n| Data source | clean-room published algorithm | Apache Commons Codec rule data | Apache Commons Codec rule data | clean-room published algorithm | clean-room published algorithm |\n\nRule of thumb: reach for Double Metaphone as a fast general-purpose default, BMPM when names cross languages or scripts, and Daitch-Mokotoff for Eastern-European and Jewish genealogy where it is the field standard. **NYSIIS** and **Match Rating Approach** are lighter, single-key English/American encoders, useful as alternate index keys or a second opinion alongside Double Metaphone.\n\n## API\n\n### Double Metaphone\n\nPrimary + alternate phonetic keys (Lawrence Philips). Clean-room implementation.\n\n```php\ndouble_metaphone(string $string, int $max_length = 4): array\n\ndouble_metaphone(\"Schwarzenegger\");        // ['primary' =\u003e 'XRSN', 'alternate' =\u003e 'XFRT']\ndouble_metaphone(\"Smith\");                 // ['primary' =\u003e 'SM0',  'alternate' =\u003e 'XMT']\ndouble_metaphone(\"Catherine\", 3);          // ['primary' =\u003e 'K0R',  'alternate' =\u003e 'KTR']\n```\n\n`alternate` equals `primary` when the algorithm produced no alternate branch. `max_length` caps each key (default 4; `0` or negative = unlimited).\n\n### Beider-Morse Phonetic Matching\n\nLanguage-aware token set. `|` separates alternatives. With the default\nconcatenation mode, ordinary words form one encoded sequence (`\"John Smith\"`\nbecomes `\"ionzmit\"`). Recognized generic prefixes use\n`(remainder)-(combined)` groups. Matches Apache Commons Codec's default\n`BeiderMorseEncoder`.\n\n```php\nbmpm(string $string, int $name_type = BMPM_GENERIC, int $accuracy = BMPM_APPROX, string $language = \"\"): string\n\nbmpm(\"Jackson\");                           // \"iakson|iaksun|...|zokson\"\nbmpm(\"Garcia\", BMPM_SEPHARDIC, BMPM_EXACT);// \"garsia|gartSa\"\n```\n\nEmpty `$language` auto-detects; pass a language name (e.g. `\"russian\"`) to force it. Constants: `BMPM_GENERIC`, `BMPM_ASHKENAZI`, `BMPM_SEPHARDIC`, `BMPM_APPROX`, `BMPM_EXACT`.\n\nA forced language also applies to the split variants of prefixed names (`van Smith`, `d'Angelo`). Commons Codec re-detects the language inside its prefix branch, silently ignoring the forced set there; this extension deliberately diverges and keeps it forced.\n\n### Daitch-Mokotoff Soundex\n\nList of distinct 6-digit codes (the algorithm branches on ambiguous letters). Matches Apache Commons Codec's `DaitchMokotoffSoundex` in branching mode.\n\n```php\ndm_soundex(string $string): array\n\ndm_soundex(\"Auerbach\");                    // ['097400', '097500']\ndm_soundex(\"Peters\");                      // ['734000', '739400']\n```\n\n### NYSIIS\n\nSingle phonetic key (New York State Identification and Intelligence System), tuned for American/English surnames. Reimplementation of the published algorithm; matches Apache Commons Codec's `Nysiis`.\n\n```php\nnysiis(string $string, int $max_length = 6): string\n\nnysiis(\"Larson\");                          // \"LARSAN\"\nnysiis(\"Larsen\");                          // \"LARSAN\"  (same key)\nnysiis(\"Macdonald\", 0);                    // \"MCDANALD\"  (full, untruncated)\n```\n\nThe classic algorithm truncates to 6 characters; `max_length = 0` (or negative) returns the full key.\n\n### Match Rating Approach\n\nCompact codex (Western Airlines, 1977). Pair it with its own similarity test instead of comparing codexes for equality.\n\n```php\nmatch_rating(string $string): string\n\nmatch_rating(\"Smith\");                     // \"SMTH\"\nmatch_rating(\"Catherine\");                 // \"CTHRN\"\n```\n\nUse `match_rating_compare()` (below) for the actual homophone decision. It applies the algorithm's length-and-rating rules that plain codex equality skips.\n\n## Comparison helpers\n\nEach encoder produces a different output shape, so \"do these sound alike?\" needs the right comparison per algorithm. These helpers encapsulate that, so you don't reimplement the set-intersection or match-strength logic in userland.\n\n```php\n// Double Metaphone: 2 = primary keys agree, 1 = an alternate crosses, 0 = no match\n// (a word-final J emits a trailing space into the alternate code, per the\n// published algorithm; compare codes as returned, don't trim them)\ndouble_metaphone_match(string $a, string $b, int $max_length = 4): int\ndouble_metaphone_match(\"Catherine\", \"Kathryn\");          // 2\ndouble_metaphone_match(\"Vagner\", \"Wagner\");              // 1\n\n// BMPM: true when the phoneme token sets intersect (same args as bmpm())\nbmpm_match(string $a, string $b, int $name_type = BMPM_GENERIC, int $accuracy = BMPM_APPROX, string $language = \"\"): bool\nbmpm_match(\"Moskowitz\", \"Moskovitz\");                    // true\n\n// Daitch-Mokotoff: true when the code sets intersect\ndm_soundex_match(string $a, string $b): bool\ndm_soundex_match(\"Moskowitz\", \"Moskovitz\");              // true\n\n// NYSIIS: true when the single keys are equal\nnysiis_match(string $a, string $b, int $max_length = 6): bool\nnysiis_match(\"Smith\", \"Schmit\");                         // true (both SNAT)\n\n// Match Rating Approach: true when the two names clear the MRA similarity threshold\nmatch_rating_compare(string $a, string $b): bool\nmatch_rating_compare(\"Catherine\", \"Kathryn\");            // true\n```\n\n## Usage\n\nFor a one-off \"do these sound alike?\" check, use the comparison helpers directly. Each applies the correct per-algorithm logic:\n\n```php\ndouble_metaphone_match(\"Catherine\", \"Kathryn\");   // 2 (strong)\ndm_soundex_match(\"Moskowitz\", \"Moskovitz\");       // true\nbmpm_match(\"Peterson\", \"Petersen\");               // true\nmatch_rating_compare(\"Catherine\", \"Kathryn\");     // true\n```\n\nFor indexed lookup, encode once and store the key(s) with each record, then query by encoded value instead of re-encoding at search time. Double Metaphone gives one or two keys per name; Daitch-Mokotoff and BMPM give a set, so index every code. BMPM separates alternatives with `|`; ordinary words are concatenated, while recognized generic prefixes produce `(remainder)-(combined)` groups:\n\n```php\n// Build a phonetic index, then look up by shared code\n$index = [];\nforeach ($records as $id =\u003e $name) {\n    foreach (dm_soundex($name) as $code) {   // index every code in the set\n        $index[$code][] = $id;\n    }\n}\n$hits = $index[dm_soundex(\"Moskovitz\")[0]] ?? [];\n\n// Splitting a BMPM token string into its individual codes. Prefixed names emit\n// grouped output with parentheses (e.g. bmpm(\"van Smith\") =\u003e \"(zmit)-(...)\"), so\n// split on '(', ')', '|' and '-' -- the same separators bmpm_match() tokenizes on.\n$codes = preg_split('/[()|-]+/', bmpm(\"van Smith\"), -1, PREG_SPLIT_NO_EMPTY);\n```\n\n## Performance\n\nSingle-name encode, warm, `-O2` non-ASan PHP 8.4 on one core, over a representative mix of 18 names (best of 5 trials). Absolute time scales with input length; the relative ordering is the stable part.\n\n| encoder | per call | throughput | relative |\n|---|---|---|---|\n| `match_rating()` | ~0.043 µs | ~23M/sec | 0.24x |\n| `nysiis()` | ~0.074 µs | ~13M/sec | 0.42x |\n| `double_metaphone()` | ~0.18 µs | ~5.5M/sec | 1.0x |\n| `dm_soundex()` | ~0.41 µs | ~2.4M/sec | ~2.3x slower |\n| `bmpm()` | ~11 µs | ~91k/sec | ~60x slower |\n\nMatch Rating and NYSIIS are short single-key passes, so they're the cheapest. Double Metaphone is a single linear pass with a primary/alternate split. Daitch-Mokotoff branches on ambiguous letters and dedups the resulting codes; a first-byte rule index keeps it fast. BMPM is the heaviest: language detection, a main transliteration pass, and two final rule passes, expanding a Cartesian product of phoneme alternatives capped at 20 per word. When you know the language, passing an explicit `$language` skips auto-detection and can cut bmpm time several-fold, though the gain depends on the chosen language's ruleset. Choose BMPM for recall, not throughput.\n\nThe comparison helpers cost roughly two encodes plus a cheap compare:\n\n| helper | per call | throughput |\n|---|---|---|\n| `match_rating_compare()` | ~0.11 µs | ~9M/sec |\n| `nysiis_match()` | ~0.14 µs | ~7M/sec |\n| `double_metaphone_match()` | ~0.26 µs | ~3.8M/sec |\n| `dm_soundex_match()` | ~0.80 µs | ~1.3M/sec |\n| `bmpm_match()` | ~22 µs | ~45k/sec |\n\nFor repeated lookups against a fixed corpus, encode once and index the keys (see [Usage](#usage)) rather than calling a helper per candidate pair.\n\n## Notes \u0026 limitations\n\n- Input is UTF-8. `bmpm()` and `dm_soundex()` fold accented Latin and lowercase both\n  Latin and Cyrillic script before rule matching, so raw `Иванов` encodes correctly.\n- **Greek-script input is a known limitation:** Greek capitals are not lowercased\n  (the algorithm's context-sensitive final-sigma cannot be expressed by a point-wise\n  case map), so pass Greek names already lowercased or romanized.\n- `double_metaphone()` targets ASCII/Latin; non-letter bytes are skipped, matching\n  Apache Commons Codec.\n- `nysiis()` and `match_rating()` operate on ASCII letters; `match_rating()`\n  also folds the Latin-1/Latin-Extended accent set the reference handles. A\n  non-ASCII letter outside that fold set (e.g. `ẞ` U+1E9E, `İ` U+0130) is\n  dropped, a deliberate divergence from Commons Codec, which keeps the raw\n  character in the codex.\n- `bmpm()`, `bmpm_match()`, `dm_soundex()`, and `dm_soundex_match()` reject\n  input longer than 4096 bytes with a `ValueError`. Real names are far shorter;\n  the cap bounds branch work and BMPM's multi-pass expansion on untrusted input.\n\nInput-length policy by function (the cap is per-argument, so both operands of a\nmatch/compare helper are checked):\n\n| Function(s) | Max input | Over the limit |\n|---|---|---|\n| `bmpm`, `bmpm_match` | 4096 bytes | throws `ValueError` |\n| `dm_soundex`, `dm_soundex_match` | 4096 bytes | throws `ValueError` |\n| `double_metaphone`, `double_metaphone_match` | not capped | encodes (bounded by memory) |\n| `nysiis`, `nysiis_match` | not capped | encodes (bounded by memory) |\n| `match_rating`, `match_rating_compare` | not capped | encodes (bounded by memory) |\n\nThe uncapped encoders run in linear time and space, so bound untrusted input at\nthe application layer if you feed them arbitrary-length strings.\n\n## 🔗 Native PHP extensions\n\nCompanion native PHP extensions:\n\n- **[php_excel](https://github.com/iliaal/php_excel)**: native Excel I/O via LibXL. 7-10× faster than PhpSpreadsheet, full XLS/XLSX with formulas, formatting, and styling.\n- **[mdparser](https://github.com/iliaal/mdparser)**: native CommonMark + GFM markdown parser via md4c. 15-30× faster than pure-PHP libraries.\n- **[php_clickhouse](https://github.com/iliaal/php_clickhouse)**: native ClickHouse client speaking the wire protocol directly. Picks up where SeasClick left off.\n- **[pdo_duckdb](https://github.com/iliaal/pdo_duckdb)**: PDO driver for DuckDB, analytical SQL in your PHP stack.\n- **[fastjson](https://github.com/iliaal/fastjson)**: drop-in faster `ext/json`, backed by yyjson. 6× encode, 2.7× decode, 5× validate.\n- **[phpser](https://github.com/iliaal/phpser)**: decoder-optimized binary serializer for cache workloads. Faster than igbinary on packed numerics and DTO batches.\n- **[fast_uuid](https://github.com/iliaal/fast_uuid)**: high-throughput UUID generation (v1/v4/v7), batched CSPRNG and SIMD hex formatter, ramsey-compatible API.\n- **[fastchart](https://github.com/iliaal/fastchart)**: native chart-rendering extension. 38 chart types behind one fluent OO API, SVG-canonical with PNG/JPG/WebP and optional PDF output.\n- **[statgrab](https://github.com/iliaal/statgrab)**: system statistics (CPU, memory, disk, network) via libstatgrab, no parsing /proc by hand.\n\n## License\n\nBSD 3-Clause (see [LICENSE](LICENSE)).\n\nThe Beider-Morse and Daitch-Mokotoff rule data is vendored from\n[Apache Commons Codec](https://commons.apache.org/proper/commons-codec/) under\nthe Apache License 2.0. The complete terms are in `LICENSE-APACHE`; the Commons\nCodec attribution is in `NOTICE`, and Section 2 of `LICENSE` maps the rule data\nto those files.\nDouble Metaphone, NYSIIS, and Match Rating Approach are independent\nimplementations of their published algorithms, validated against (and\nedge-case-aligned with) Apache Commons Codec as the parity-test oracle\n(no third-party code or data).\n\n---\n\n[Follow @iliaa on X](https://x.com/iliaa) • [Blog](https://ilia.ws) • If this matched the names exact comparison missed, ⭐ star it!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filiaal%2Fphonetic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Filiaal%2Fphonetic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filiaal%2Fphonetic/lists"}