Card embossed name validation

Card Embossed Name Validation

Overview

ConnectPay automatically checks the embossed name on every Create Card Application against the cardholder’s verified name and calculates one similarity score for the full name. Following description is focused on Consumer program score of 0.80. At 0.80 or higher the name is approved automatically. Below 0.80, the name goes to manual review if it meets the minimum requirement, and is rejected if it does not.

Validation flow

  1. You submit Create Card Application with embossing.firstName and embossing.lastName.
  2. ConnectPay compares the full embossed name (first name and last name together) with the cardholder’s full name and calculates one Jaro-Winkler score.
  3. Score 0.80 or higher: auto approved, card creation continues.
  4. Score below 0.80, minimum requirement met: the application is held for manual review.
  5. Score below 0.80, minimum requirement not met: the application is rejected.

Name requirements

RuleValue
Minimum requirementOne full first name and one full last name of the cardholder
Can be left outMiddle names, extra given names, extra parts of a compound surname
Does not meet the minimumInitials instead of the first name, shortened or truncated names, nicknames, last name as an initial, first and last name swapped
Field limitsfirstName 1–12 characters, lastName 1–13 characters
Ignored in the comparisonLetter case, diacritics, hyphens, apostrophes, dots

If a full name is longer than the field limit, use another given name or surname part that fits. If the name or surname exceeds the allowed symbol count, truncate from the end until it fits.

Examples

Cardholder nameEmbossed nameScoreMinimum metResult
Jonas PetraitisJONAS PETRAITIS1.00YesAuto approved
Živilė KazlauskienėZivile Kazlauskiene1.00YesAuto approved
Łukasz SørensenLukasz Sorensen1.00YesAuto approved
Jonas PetraitisJonas Petraitys0.97YesAuto approved
Anna Maria SvenssonAnna M Svensson0.96YesAuto approved
Anna Maria SvenssonAnna Svensson0.94YesAuto approved
Jonas PetraitisJon Petraitis0.92NoAuto approved
Anna Maria García LópezAnna Lopez0.74YesManual review
Anna Maria García LópezMaria Garcia0.73YesManual review
Jonas PetraitisJ. Petraitis0.78NoRejected
Robert JonesBob Jones0.73NoRejected
Jonas PetraitisPetraitis Jonas0.73NoRejected

The score compares the full embossed name with the cardholder’s full name, so leaving out a middle name or a surname part lowers it. The minimum requirement only decides the outcome when the score is below 0.80.

Recommendation: check the name before you submit

Run the same check in your own system so applications pass automatically and avoid manual review delays.

  1. Pre-fill the embossed name from the cardholder’s verified first name and last name.
  2. If the cardholder edits it, run the check and warn them when the score is below 0.80.
  3. Ask the cardholder to correct the name before you submit Create Card Application.

Always compare against the name ConnectPay holds for the cardholder, not a name from another source.

Business white label program differences

To be Updated.

Implementing the check in your system

The check has three steps: normalise both names, build the accepted forms of the cardholder’s name, and check that the embossed value holds a full name that scores at least 0.80 with Jaro-Winkler.

Step 1: Normalise both names

Apply the same normalisation to the embossed value and to the cardholder’s name:

  1. Map letters that do not decompose, such as Ø → O, Ł → L, ß → SS.
  2. Remove diacritics (Unicode NFKD, then drop combining marks): Ž → Z, ė → e.
  3. Convert to upper case.
  4. Replace the separators - ' . / , & with a space.
  5. Remove any remaining character that is not A–Z or a space, then collapse repeated spaces.

Step 2: Build the accepted forms of the cardholder’s name

After normalising, split the cardholder’s first name and last name into words. Each word is a full name the embossed value may use, which covers left-out middle names and compound surnames. Do not add initials or truncated forms to this list.

Step 3: Score the embossed value

  1. Split the embossed value into words. Single letters are initials; longer words are name words.
  2. If the value has no name word, only initials, it fails. Initials are accepted only in the first name, after a full name word, and must match the first letter of one of the cardholder’s names.
  3. Score each name word with Jaro-Winkler against the cardholder’s words. A word more than one character shorter than the name it is compared with counts as a shortened name and scores 0.
  4. The part’s score is the lowest score among its name words. The name passes when the first name score and the last name score are both 0.80 or higher.

Use the standard Jaro-Winkler parameters: prefix scale 0.1 and a common prefix of up to 4 characters. Libraries such as Apache Commons Text (JaroWinklerSimilarity), RapidFuzz and jellyfish use these defaults. Check that your library returns a similarity (1 = identical) and not a distance (0 = identical).

Reference implementation (Java)

import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public final class EmbossedNameCheck {

    public static final double THRESHOLD = 0.80;

    public enum Outcome { AUTO_APPROVED, MANUAL_REVIEW, REJECTED }

    public record Result(double score, boolean minimumMet, Outcome outcome) { }

    // Letters that Unicode decomposition does not reduce to A-Z
    private static final Map<Character, String> EXTRA = Map.ofEntries(
            Map.entry('Ø', "O"), Map.entry('ø', "o"), Map.entry('Ł', "L"), Map.entry('ł', "l"),
            Map.entry('Đ', "D"), Map.entry('đ', "d"), Map.entry('ß', "SS"),
            Map.entry('Æ', "AE"), Map.entry('æ', "ae"), Map.entry('Œ', "OE"), Map.entry('œ', "oe"));

    private EmbossedNameCheck() { }

    /** Full check: one score for the whole name, then the minimum requirement below the threshold. */
    public static Result check(String embFirst, String embLast, String holderFirst, String holderLast) {
        double score = jaroWinkler(normalize(embFirst + " " + embLast),
                                   normalize(holderFirst + " " + holderLast));
        boolean minimumMet = scorePart(embFirst, holderFirst, true) >= THRESHOLD
                          && scorePart(embLast, holderLast, false) >= THRESHOLD;
        Outcome outcome = score >= THRESHOLD ? Outcome.AUTO_APPROVED
                        : minimumMet ? Outcome.MANUAL_REVIEW
                        : Outcome.REJECTED;
        return new Result(Math.round(score * 100) / 100.0, minimumMet, outcome);
    }

    public static double jaro(String s1, String s2) {
        if (s1.equals(s2)) return 1.0;
        int l1 = s1.length(), l2 = s2.length();
        if (l1 == 0 || l2 == 0) return 0.0;
        int window = Math.max(Math.max(l1, l2) / 2 - 1, 0);
        boolean[] m1 = new boolean[l1], m2 = new boolean[l2];
        int matches = 0;
        for (int i = 0; i < l1; i++) {
            for (int j = Math.max(0, i - window); j < Math.min(i + window + 1, l2); j++) {
                if (!m2[j] && s1.charAt(i) == s2.charAt(j)) {
                    m1[i] = m2[j] = true;
                    matches++;
                    break;
                }
            }
        }
        if (matches == 0) return 0.0;
        int t = 0, k = 0;
        for (int i = 0; i < l1; i++) {
            if (m1[i]) {
                while (!m2[k]) k++;
                if (s1.charAt(i) != s2.charAt(k)) t++;
                k++;
            }
        }
        t /= 2;
        return ((double) matches / l1 + (double) matches / l2 + (double) (matches - t) / matches) / 3.0;
    }

    public static double jaroWinkler(String s1, String s2) {
        double j = jaro(s1, s2);
        int prefix = 0;
        int max = Math.min(Math.min(s1.length(), s2.length()), 4);
        while (prefix < max && s1.charAt(prefix) == s2.charAt(prefix)) prefix++;
        return j + prefix * 0.1 * (1 - j);
    }

    /** Uppercase, strip diacritics, keep letters A-Z and single spaces only. */
    public static String normalize(String name) {
        StringBuilder sb = new StringBuilder();
        for (char c : name.toCharArray()) sb.append(EXTRA.getOrDefault(c, String.valueOf(c)));
        return Normalizer.normalize(sb.toString(), Normalizer.Form.NFKD)
                .replaceAll("\\p{M}", "")
                .toUpperCase(Locale.ROOT)
                .replaceAll("[-'./,&]", " ")
                .replaceAll("[^A-Z ]", "")
                .replaceAll("\\s+", " ")
                .trim();
    }

    /** Best score of one embossed word; a word more than one character shorter than the name it matches scores 0. */
    private static double fullWordScore(String word, List<String> holderWords) {
        double best = 0.0;
        for (String h : holderWords) {
            if (word.length() >= h.length() - 1) best = Math.max(best, jaroWinkler(word, h));
        }
        return best;
    }

    /** Minimum-requirement score of one field: 0 if it holds no full name. */
    private static double scorePart(String embossed, String holderPart, boolean allowExtraInitials) {
        List<String> embWords = words(normalize(embossed));
        List<String> holderWords = words(normalize(holderPart));
        if (embWords.isEmpty() || holderWords.isEmpty()) return 0.0;
        List<String> full = new ArrayList<>(), initials = new ArrayList<>();
        for (String w : embWords) (w.length() > 1 ? full : initials).add(w);
        if (full.isEmpty()) return 0.0;                      // an initial alone never meets the minimum
        if (!initials.isEmpty()) {
            if (!allowExtraInitials) return 0.0;
            for (String i : initials) {                       // an extra initial must belong to the holder's names
                if (holderWords.stream().noneMatch(h -> h.charAt(0) == i.charAt(0))) return 0.0;
            }
        }
        double min = 1.0;
        for (String w : full) min = Math.min(min, fullWordScore(w, holderWords));
        return min;
    }

    private static List<String> words(String s) {
        return s.isEmpty() ? List.of() : Arrays.asList(s.split(" "));
    }
}

Example calls:

EmbossedNameCheck.check("Anna M", "Svensson", "Anna Maria", "Svensson");
// Result[score=0.96, minimumMet=true, outcome=AUTO_APPROVED]

EmbossedNameCheck.check("Maria", "Garcia", "Anna Maria", "García López");
// Result[score=0.73, minimumMet=true, outcome=MANUAL_REVIEW]

EmbossedNameCheck.check("J.", "Petraitis", "Jonas", "Petraitis");
// Result[score=0.78, minimumMet=false, outcome=REJECTED]

Requires Java 16 or later (records). No external libraries.

Test your implementation

Run your code against the cases in the Examples table: every Automatic pass row must pass your check and every Manual review row must fail it. Also check the textbook values for plain Jaro-Winkler: MARTHA vs MARHTA = 0.961 and DIXON vs DICKSONX = 0.813.

A name that passes your check should pass ConnectPay’s automatic check. A name that fails is not rejected, but it goes to manual review and delays the card.

FAQ

Can the cardholder use a nickname or short form on the card? It will not pass the automatic check, so Mike for Michael or Jon for Jonas goes to manual review. Spelling variants of the full name, such as a transliteration, pass automatically if they score 0.80 or higher.

Does letter case or punctuation matter? No. Names are compared in upper case without diacritics, and hyphens, apostrophes and dots are treated as spaces.

What if the cardholder’s legal name is longer than the field limits? Use another full given name or surname part that fits within 12 characters for the first name and 13 for the last name. Truncated names go to manual review.

Is the company name on Visa Business cards validated the same way? No, this check covers the cardholder’s first name and last name only.

Can we lower the threshold for our program? No. The 80% threshold applies to all programs.

Scroll to Top