Phonetic matching of names with fullmetaphone

library(fullmetaphone)

The problem

Names are written down by people, and people spell the same name in different ways. Meyer, Meier, Mayer and Maier are one German surname. Catherine and Katherine are one first name. Stephen Phillips and Steven Philips may well be one person. Any join, lookup or duplicate check that relies on exact equality of names will miss these.

Phonetic algorithms address this by turning a word into a code that stands for how it sounds. Words that sound alike get the same code, so records can be compared by code instead of by spelling. The idea goes back to Soundex, patented in 1918 and used for the United States Census. Lawrence Philips improved on it with Metaphone in 1990, which follows the rules of English spelling far more closely. In 2000 he published Double Metaphone, which also handles names of German, Slavic, Italian, Spanish, French, Greek and Chinese origin, and which gives a second code where a name has two common pronunciations.

This package provides both algorithms in R. Its main difference from other implementations is the length of the codes. Most implementations cut every code to four characters. Here codes keep up to 32 characters by default, which is the complete code for practically every real name, and the length can be set by the user. The sections below explain why that matters and show how to use the codes in common data tasks.

Getting started

The package has three functions.

Function Returns
metaphone(x) A character vector with one Metaphone code per element of x.
double_metaphone(x) A data frame with a primary and a secondary Double Metaphone code per element of x.
sounds_like(x, y) A logical vector, TRUE where x and y share a code.

They share the following arguments.

Argument Default Meaning
max_length 32 The largest number of characters kept in each code. Use Inf for no limit, or 4 for the traditional short codes. See Choosing the code length.
by_word FALSE If TRUE, each word of a name is encoded separately and the codes are joined by spaces. See Names with several words.
method "double" sounds_like() only. "double" compares Double Metaphone codes, "metaphone" compares Metaphone codes.
names <- c("Meyer", "Meier", "Mayer", "Catherine", "Katherine",
           "Smith", "Schmidt")
metaphone(names)
#> [1] "MYR"   "MR"    "MYR"   "K0RN"  "K0RN"  "SM0"   "SKMTT"
double_metaphone(names)
#>   primary secondary
#> 1      MR        MR
#> 2      MR        MR
#> 3      MR        MR
#> 4    K0RN      KTRN
#> 5    K0RN      KTRN
#> 6     SM0       XMT
#> 7     XMT       SMT

All functions accept character vectors of any length and return one result per element. Missing values give NA. Each distinct value is encoded only once, so columns with many repeats are fast.

Choosing the code length

The argument max_length controls how many characters of each code are kept. A code has roughly one character for each consonant sound, so it grows with the length of the name.

long_name <- "Christopher Alexander Montgomery Fitzgerald"
double_metaphone(long_name)
#>                     primary                 secondary
#> 1 KRSTFRLKSNTRMNTKMRFTSKRLT KRSTFRLKSNTRMNTKMRFTSJRLT
double_metaphone(long_name, max_length = 4)
#>   primary secondary
#> 1    KRST      KRST
double_metaphone(long_name, max_length = 10)
#>      primary  secondary
#> 1 KRSTFRLKSN KRSTFRLKSN
double_metaphone(long_name, max_length = Inf)
#>                     primary                 secondary
#> 1 KRSTFRLKSNTRMNTKMRFTSKRLT KRSTFRLKSNTRMNTKMRFTSJRLT

There are three common choices.

With by_word = TRUE the limit applies to each word separately.

double_metaphone(long_name, by_word = TRUE, max_length = 4)
#>               primary           secondary
#> 1 KRST ALKS MNTK FTSK KRST ALKS MNTK FTSJ

Codes made with different lengths cannot be compared with each other, so use the same max_length for all data that you want to match.

How the codes are built

A code keeps the consonant sounds of a word and drops most vowels. Some rules are easy to recognise. A “ph” sounds like F, initial “kn”, “gn” and “wr” lose their silent first letter, and “th” becomes the digit 0 because it looks like the Greek letter theta.

rules <- c("Philip", "Knight", "Wright", "Gnome", "Thomas", "Laugh",
           "Dumb", "Edge", "Xavier")
data.frame(word = rules, metaphone = metaphone(rules),
           double_metaphone(rules))
#>     word metaphone primary secondary
#> 1 Philip       FLP     FLP       FLP
#> 2 Knight        NT      NT        NT
#> 3 Wright        RT      RT        RT
#> 4  Gnome        NM      NM        NM
#> 5 Thomas       0MS     TMS       TMS
#> 6  Laugh         L      LF        LF
#> 7   Dumb        TM      TM        TM
#> 8   Edge        EJ      AJ        AJ
#> 9 Xavier       SFR      SF       SFR

Double Metaphone has many more rules than Metaphone, mostly for names from other languages. Where a name is pronounced differently in its language of origin and in English, the secondary code gives the second reading.

foreign <- c("Schmidt", "Michael", "Jose", "Tagliaro", "Czerny",
             "Wasserman", "Zhao", "Breaux")
data.frame(word = foreign, double_metaphone(foreign))
#>        word primary secondary
#> 1   Schmidt     XMT       SMT
#> 2   Michael     MKL       MXL
#> 3      Jose      HS        HS
#> 4  Tagliaro    TKLR       TLR
#> 5    Czerny     SRN       XRN
#> 6 Wasserman   ASRMN     FSRMN
#> 7      Zhao       J         J
#> 8    Breaux      PR        PR

So “Schmidt” has the primary code XMT, as it is said in German, and the secondary code SMT, as an English speaker might say it. “Smith” has the codes SM0 and XMT. The two names share XMT, so they are treated as possible matches. Philips recommended exactly this comparison. Two names match if any code of one equals any code of the other, and sounds_like() applies it.

sounds_like("Smith", c("Schmidt", "Smyth", "Smythe", "Jones"))
#> [1]  TRUE  TRUE  TRUE FALSE
sounds_like("Wasserman", "Vasserman")
#> [1] TRUE

The original Metaphone gives only one code, so it is stricter.

sounds_like("Smith", "Schmidt", method = "metaphone")
#> [1] FALSE

Why full-length codes

This section compares the default codes with codes cut to four characters, using max_length = 4. Philips’ original programs, and most later implementations such as PGRdup::DoubleMetaphone(), return only the first four characters of each code. Four characters were enough for building search indexes of English surnames in the 1990s. They are not enough for many tasks today, because different names that merely start alike end up with the same code.

The package includes us_surnames, the 5000 most common surnames in the 2010 United States Census.

head(us_surnames)
#>    surname rank   count
#> 1    Smith    1 2442977
#> 2  Johnson    2 1932812
#> 3 Williams    3 1625252
#> 4    Brown    4 1437026
#> 5    Jones    5 1425470
#> 6   Garcia    6 1166120
codes_full <- double_metaphone(us_surnames$surname)$primary
codes_four <- double_metaphone(us_surnames$surname, max_length = 4)$primary
length(unique(codes_full))
#> [1] 2344
length(unique(codes_four))
#> [1] 1957

Even for single surnames, which are short, cutting the codes to four characters removes almost 400 distinct codes, merging surnames that sound different. Here is one four-character code and the surnames behind it.

group <- codes_four == "ALTR"
data.frame(surname = us_surnames$surname[group],
           four = codes_four[group],
           full = codes_full[group])
#>       surname four   full
#> 1     Walters ALTR  ALTRS
#> 2      Walter ALTR   ALTR
#> 3      Wilder ALTR   ALTR
#> 4       Elder ALTR   ALTR
#> 5    Eldridge ALTR  ALTRJ
#> 6    Aldridge ALTR  ALTRJ
#> 7     Aldrich ALTR  ALTRX
#> 8     Waldron ALTR  ALTRN
#> 9     Waldrop ALTR  ALTRP
#> 10   Alderman ALTR ALTRMN
#> 11 Wooldridge ALTR  ALTRJ

With four characters, Walter, Waldron, Aldrich and Alderman look alike. With full codes, only Walter, Wilder and Elder remain together.

The effect is much stronger for full names, which are what most record linkage tasks deal with. The example below pairs common first names from several countries with the census surnames.

first_names <- c("James", "Mary", "Robert", "Patricia", "John",
                 "Jennifer", "Michael", "Linda", "David", "Elizabeth",
                 "Christopher", "Sarah", "Maria", "Jose", "Juan",
                 "Mohammed", "Ahmed", "Wei", "Anna", "Sophie", "Lukas",
                 "Pierre", "Giovanni", "Olga", "Katarzyna", "Priya")
set.seed(2026)
people <- unique(paste(sample(first_names, 5000, replace = TRUE),
                       sample(us_surnames$surname, 5000, replace = TRUE)))
length(people)
#> [1] 4914
length(unique(double_metaphone(people)$primary))
#> [1] 4243
length(unique(double_metaphone(people, max_length = 4)$primary))
#> [1] 875

With four-character codes, the code of a full name is little more than the code of the first name. Almost 5000 different people share fewer than 900 codes, so on average about five or six unrelated people would be flagged as one. Full codes keep most of them apart.

cr <- c("Christopher Anderson", "Christina Andrews", "Christopher Andersen")
data.frame(name = cr,
           four = double_metaphone(cr, max_length = 4)$primary,
           full = double_metaphone(cr)$primary)
#>                   name four        full
#> 1 Christopher Anderson KRST KRSTFRNTRSN
#> 2    Christina Andrews KRST   KRSTNNTRS
#> 3 Christopher Andersen KRST KRSTFRNTRSN

Example 1. Finding duplicate records

A typical task is to find records in one table that may describe the same person. In this small contact list, several people were entered twice with different spellings.

contacts <- data.frame(
  id    = 1:12,
  first = c("Catherine", "Katherine", "Stephen", "Steven", "Mohammed",
            "Muhammad", "Jeffrey", "Geoffrey", "Anne", "Ann", "Sean",
            "Shaun"),
  last  = c("Smith", "Smyth", "Phillips", "Philips", "Ali", "Aly",
            "Wasserman", "Vasserman", "Thompson", "Thomson", "O'Neill",
            "O'Neil"),
  city  = c("Leeds", "Leeds", "Boston", "Boston", "Toronto", "Toronto",
            "Chicago", "Chicago", "Sydney", "Sydney", "Dublin", "Dublin")
)

Comparing every record with every other record is fine for a few hundred rows, but for large tables it is too slow. The usual approach, called blocking, first groups records that share a cheap key and only compares records within a group. A natural first try is to use the primary code of the surname as the key, together with the city.

contacts$key <- paste(double_metaphone(contacts$last)$primary,
                      contacts$city)
pairs <- merge(contacts, contacts, by = "key", suffixes = c("_a", "_b"))
pairs <- pairs[pairs$id_a < pairs$id_b, ]
pairs$first_match <- sounds_like(pairs$first_a, pairs$first_b)
pairs[, c("first_a", "last_a", "first_b", "last_b", "first_match")]
#>      first_a   last_a   first_b  last_b first_match
#> 2   Mohammed      Ali  Muhammad     Aly        TRUE
#> 6       Sean  O'Neill     Shaun  O'Neil       FALSE
#> 11   Stephen Phillips    Steven Philips        TRUE
#> 16 Catherine    Smith Katherine   Smyth        TRUE

Four pairs share a block, and in three of them the first names also sound alike. The Wasserman pair is missing, because “Wasserman” and “Vasserman” have different primary codes, ASRMN and FSRMN. They share only a secondary code. A key built from one code cannot see that, while sounds_like() compares all four combinations of primary and secondary codes. So a better plan is to block on something simpler, here the city, and to compare names with sounds_like() inside each block.

pairs <- merge(contacts, contacts, by = "city", suffixes = c("_a", "_b"))
pairs <- pairs[pairs$id_a < pairs$id_b, ]
same_person <- sounds_like(pairs$last_a, pairs$last_b) &
  sounds_like(pairs$first_a, pairs$first_b)
pairs[same_person, c("id_a", "first_a", "last_a", "id_b", "first_b", "last_b")]
#>    id_a   first_a    last_a id_b   first_b    last_b
#> 2     3   Stephen  Phillips    4    Steven   Philips
#> 6     7   Jeffrey Wasserman    8  Geoffrey Vasserman
#> 14    1 Catherine     Smith    2 Katherine     Smyth
#> 22    5  Mohammed       Ali    6  Muhammad       Aly

Now four of the six planted duplicates are found. Jeffrey and Geoffrey match through the shared code JFR. The other two pairs show the limits of the method. “Thompson” and “Thomson” get different codes, TMPSN and TMSN, because the P in Thompson is coded although it is barely pronounced. “Sean” and “Shaun” differ because the Irish “Se” is read by the English rule as S. Phonetic codes reduce the problem of spelling variation, but they do not remove it. The section on good practice below suggests ways to catch more.

Example 2. Linking two tables

The same technique links records across two sources, for example a membership list and a donations file kept by different staff.

members <- data.frame(
  member_id = c("M1", "M2", "M3", "M4", "M5"),
  name = c("Jon Schmidt", "Maria Gonzalez", "Peter Kowalski",
           "Claire Dubois", "Hans Meyer")
)
donations <- data.frame(
  receipt = c("R1", "R2", "R3", "R4", "R5", "R6"),
  donor = c("John Smith", "Mariah Gonzales", "Piotr Kowalsky",
            "Clare Du Bois", "Hans Maier", "Helen Mayer"),
  amount = c(50, 120, 75, 200, 30, 90)
)

Split the names into first and last names, then block on the surname code.

split_name <- function(x) {
  parts <- strsplit(x, " ", fixed = TRUE)
  data.frame(first = vapply(parts, `[`, "", 1),
             last  = vapply(parts, function(p) paste(p[-1], collapse = " "), ""))
}
members   <- cbind(members, split_name(members$name))
donations <- cbind(donations, split_name(donations$donor))

members$block   <- double_metaphone(members$last)$primary
donations$block <- double_metaphone(donations$last)$primary
candidates <- merge(members, donations, by = "block")
candidates[, c("name", "donor", "block")]
#>             name           donor block
#> 1 Peter Kowalski  Piotr Kowalsky  KLSK
#> 2 Maria Gonzalez Mariah Gonzales KNSLS
#> 3     Hans Meyer      Hans Maier    MR
#> 4     Hans Meyer     Helen Mayer    MR
#> 5  Claire Dubois   Clare Du Bois    TP

Joining on the primary code alone misses names whose primary codes differ but whose secondary codes match, such as “Schmidt” and “Smith”. As in the first example, it is more thorough to compare names with sounds_like(), which checks all four combinations of codes. With small tables every member can be compared with every donor. With large ones, block on something broader first, such as the first letter of the surname or a postcode.

all_pairs <- merge(members, donations, by = NULL)
all_pairs <- all_pairs[sounds_like(all_pairs$last.x, all_pairs$last.y) &
                       sounds_like(all_pairs$first.x, all_pairs$first.y), ]
all_pairs[, c("member_id", "name", "receipt", "donor", "amount")]
#>    member_id           name receipt           donor amount
#> 1         M1    Jon Schmidt      R1      John Smith     50
#> 7         M2 Maria Gonzalez      R2 Mariah Gonzales    120
#> 13        M3 Peter Kowalski      R3  Piotr Kowalsky     75
#> 19        M4  Claire Dubois      R4   Clare Du Bois    200
#> 25        M5     Hans Meyer      R5      Hans Maier     30

Here merge(..., by = NULL) forms all combinations, which is only sensible for small tables. To rank the candidate pairs, a string distance such as the generalised Levenshtein distance from base R’s adist() is a useful second opinion.

all_pairs$distance <- diag(adist(all_pairs$name, all_pairs$donor))
all_pairs[order(all_pairs$distance),
          c("name", "donor", "distance")]
#>              name           donor distance
#> 7  Maria Gonzalez Mariah Gonzales        2
#> 25     Hans Meyer      Hans Maier        2
#> 19  Claire Dubois   Clare Du Bois        3
#> 13 Peter Kowalski  Piotr Kowalsky        4
#> 1     Jon Schmidt      John Smith        5

Small distances suggest near-identical spellings. Larger ones, such as “Jon Schmidt” and “John Smith”, are worth a closer look by a person before the records are merged.

Example 3. Searching a register by sound

Genealogists, call centres and archivists often need to look up a name that was heard rather than read. sounds_like() with a single name on one side searches a whole register at once.

hits <- us_surnames[sounds_like("Meyer", us_surnames$surname), ]
head(hits[order(-hits$count), ], 10)
#>      surname rank  count
#> 18     Moore   18 724374
#> 136   Murray  136 184910
#> 183    Meyer  183 150895
#> 583     Mora  583  57464
#> 594   Morrow  594  56840
#> 757  Murillo  757  45558
#> 880    Mayer  880  39555
#> 935    Moyer  935  37050
#> 1391   Meier 1391  25486
#> 2106    Mohr 2106  17180

The search finds the expected spellings, Meyer, Mayer, Meier, Maier and Moyer, but also Moore, Murray and others. Double Metaphone drops vowels, so all names with an M followed by an R sound fall together. Full-length codes keep longer names apart, but for short names a phonetic search gives a candidate list to review, not a final answer. Adding a string distance helps to put the closest spellings first.

hits$distance <- adist("Meyer", hits$surname)[1, ]
head(hits[order(hits$distance, -hits$count), ], 6)
#>      surname rank  count distance
#> 183    Meyer  183 150895        0
#> 880    Mayer  880  39555        1
#> 935    Moyer  935  37050        1
#> 1391   Meier 1391  25486        1
#> 2557   Maier 2557  14135        2
#> 2106    Mohr 2106  17180        3

Names with several words

By default the whole text is encoded as one string, exactly as the original algorithm does. Spaces are kept while encoding, and a few rules depend on them, for example the Spanish “San” or the Dutch and German “Van” and “Von”. The resulting code runs the words together.

With by_word = TRUE each word is encoded separately and the codes are joined by spaces. This keeps the structure of the name and lets you compare the parts on their own.

double_metaphone("Maria Gonzalez Lopez")
#>      primary  secondary
#> 1 MRKNSLSLPS MRKNSLSLPS
double_metaphone("Maria Gonzalez Lopez", by_word = TRUE)
#>        primary    secondary
#> 1 MR KNSLS LPS MR KNSLS LPS

Whole-string codes are useful when the spacing of a name varies, as in “Du Bois” and “Dubois”. Word-by-word codes are useful when the order of the parts varies, as in many registers that record the family name first. Sorting the word codes makes the comparison independent of order.

sounds_like("Pierre Dubois", "Pierre Du Bois")
#> [1] TRUE
codes <- metaphone(c("Wei Zhang", "Zhang Wei"), by_word = TRUE)
sorted <- vapply(strsplit(codes, " "),
                 function(w) paste(sort(w), collapse = " "), "")
sorted
#> [1] "SHNK W" "SHNK W"

Working with data frames and the tidyverse

double_metaphone() returns a data frame, so its result can be bound to an existing table with cbind().

staff <- data.frame(name = c("Stephen Phillips", "Steven Philips"))
cbind(staff, double_metaphone(staff$name))
#>               name  primary secondary
#> 1 Stephen Phillips STFNFLPS  STFNFLPS
#> 2   Steven Philips STFNFLPS  STFNFLPS

In ‘dplyr’, a data frame returned inside mutate() without a name is unpacked into separate columns.

library(dplyr, warn.conflicts = FALSE)
mutate(staff,
       double_metaphone(name),
       metaphone = metaphone(name))
#>               name  primary secondary metaphone
#> 1 Stephen Phillips STFNFLPS  STFNFLPS  STFNFLPS
#> 2   Steven Philips STFNFLPS  STFNFLPS  STFNFLPS

Good practice and limits

Phonetic codes work best as one step in a matching process, not as the whole of it. The following points come up in almost every real project.

Credits

Lawrence Philips designed both algorithms. The Double Metaphone rules in this package are a translation of the C implementation by Maurice Aubrey in the Perl module Text::DoubleMetaphone, which includes fixes by Kevin Atkinson. The Metaphone rules and the reference test data come from Apache Commons Codec. The us_surnames data come from the United States Census Bureau.

References

Philips, L. (1990). Hanging on the metaphone. Computer Language, 7(12), 38-43.

Philips, L. (2000). The double metaphone search algorithm. C/C++ Users Journal, 18(6), 38-43. Archived copy.