Guide

Bilingual Name Normalization and Fuzzy Matching for GCC KYC

Handle Arabic and English name variations in a KYC onboarding flow without treating a fuzzy score as proof of identity.

Why names differ

Arabic names can be entered in Arabic script or transliterated into Latin script. Transliteration is not one-to-one: محمد may appear as Mohammed, Mohammad, Mohamed, or Muhammad. Spaces, hyphens, doubled vowels, and name particles such as Al, El, bin, and ibn also vary.

Names may also be abbreviated, reordered, or missing a middle or family name. Preserve each field and its source (for example, given, father, grandfather, and family name) instead of joining everything into an irreversible string. Do not assume a shared spelling implies the same person.

Normalize for comparison, not storage

KhaleejiAPI's /api/v1/arabic/process endpoint accepts the normalize operation. It removes Arabic diacritics and tatweel, folds أ/إ/آ to ا, maps ة to ه and ى to ي, and trims and collapses whitespace.

These folds can erase distinctions. Keep the original value unchanged, retain script and source metadata, and use normalized text only as a derived candidate-generation or comparison value. In particular, do not overwrite a legal name with a normalized form.

json
{
"operation": "normalize",
"text": "مُحَمَّدٌ"
}

Use fuzzy matching as a review aid

The API's name_match operation applies Arabic, Latin, or cross-script normalization and compares the full strings using normalized Levenshtein edit distance. It returns a matchScore from 0 to 1, the selected method, and an isMatch convenience flag based on the endpoint's current 0.72 threshold. The score is not a probability, and that threshold is not a KYC policy.

Whole-string similarity can overlook token order, omitted middle/family names, or collisions between common names. For consequential decisions, compare appropriately verified name components and their order, use your institution's approved policy, and send ambiguous or incomplete results to a trained reviewer. Never auto-approve or reject identity solely from isMatch or a score cutoff.

json
{
"operation": "name_match",
"text": "محمد عبد الله",
"options": {
"compareTo": "Mohammed Abdullah"
}
}

Node.js example

Run this on a trusted server. Store the API key in a server-side secret, grant the utilities:arabic scope, and never expose the key in browser code.

typescript
const apiKey = process.env.KHALEEJI_API_KEY
if (!apiKey) throw new Error("KHALEEJI_API_KEY is not configured")
function requireName(value) {
if (typeof value !== "string" || !value.trim()) {
throw new Error("Name must not be empty")
}
return value
}
async function processArabic(body) {
const response = await fetch("https://khaleejiapi.dev/api/v1/arabic/process", {
method: "POST",
headers: {
Authorization: "Bearer " + apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10_000),
})
const payload = await response.json()
if (!response.ok || !payload.data) {
throw new Error(payload.error?.message ?? `Arabic API returned HTTP ${response.status}`)
}
return payload.data
}
const enteredName = "محمد عبد الله"
const officialName = "Mohammed Abdullah"
// Keep the raw value. This normalized value is only a comparison/search aid.
const cleanedArabic = await processArabic({
operation: "normalize",
text: requireName(enteredName),
})
// name_match normalizes both values and selects Arabic, Latin, or cross-script matching.
const comparison = await processArabic({
operation: "name_match",
text: requireName(enteredName),
options: { compareTo: requireName(officialName) },
})
console.log({
cleanedArabic: cleanedArabic.result,
score: comparison.matchScore,
method: comparison.method,
})

Python example

This example uses Python's standard library and reads the key from the server environment.

python
import json
import os
from urllib.request import Request, urlopen
API_KEY = os.environ["KHALEEJI_API_KEY"]
ENDPOINT = "https://khaleejiapi.dev/api/v1/arabic/process"
def require_name(value):
if not isinstance(value, str) or not value.strip():
raise ValueError("Name must not be empty")
return value
def process_arabic(body):
request = Request(
ENDPOINT,
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={
"Authorization": "Bearer " + API_KEY,
"Content-Type": "application/json",
},
method="POST",
)
with urlopen(request, timeout=10) as response:
payload = json.loads(response.read())
if "data" not in payload:
raise RuntimeError(payload.get("error", {}).get("message", "Arabic API request failed"))
return payload["data"]
entered_name = "محمد عبد الله"
official_name = "Mohammed Abdullah"
# Preserve entered_name; use the normalized value only as a derived comparison field.
cleaned = process_arabic({"operation": "normalize", "text": entered_name})
comparison = process_arabic({
"operation": "name_match",
"text": require_name(entered_name),
"options": {"compareTo": require_name(official_name)},
})
print({
"cleaned_arabic": cleaned["result"],
"score": comparison["matchScore"],
"method": comparison["method"],
})

Production checklist

  • Keep the original submitted name, script, source, and field boundaries for audit.
  • Normalize consistently on both sides; do not compare Arabic and Latin as raw code points.
  • Test spelling variants, empty values, reordered tokens, and missing name components.
  • Use scores to prioritize review, not to decide eligibility, identity, or account access.
  • Apply consent, access control, encryption, and retention rules to identity data.