Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

Address Autocomplete for Tunisia: Arabic and Arabizi

Ask ten people in Tunis to type the same address and you'll see three different alphabets: French: Rue de Marseille, Tunis Arabic script: نهج مرسيليا Arabizi, Tunisian Arabic written in Latin letters, with digits sta

Address Autocomplete for Tunisia: Arabic and Arabizi

Ask ten people in Tunis to type the same address and you'll see three different alphabets:

  • French: Rue de Marseille, Tunis
  • Arabic script: نهج مرسيليا
  • Arabizi, Tunisian Arabic written in Latin letters, with digits standing in for sounds French doesn't have: nahj marseille, zan9et el bacha, 9asr said

An address field that only understands the first one is going to lose customers at checkout. In this post we'll build an autocomplete with the TMaps Geocoding API that copes with all three, and that displays Arabic results properly, from right to left.

Try it live: examples.tmaps.tn/04-geocoding-search. Switch the language to Arabic, or type 9asr said.

Or play with it right here, and fork it on CodePen to change the code:

Arabic search returning Arabic addresses, right to left

Arabizi in 30 seconds

Arabizi borrows digits that look like the Arabic letter they replace:

Digit Arabic Sound Example Usual French spelling
3 ع ʕayn 3in Ain
5 خ kh 5ayreddine Khayreddine
7 ح a strong h 7ammamet Hammamet
9 ق q 9asr said Ksar Said

Add the dialect words for street types, nahj (rue), zan9a (impasse), chare3 (avenue) and 7ay (cité), and you've got the way a lot of people actually write addresses here.

Step 1: a solid basic autocomplete

At its core, the autocomplete calls GET /geocoding/forward as the user types. Three details make the difference:

let timer;
let controller;

input.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(search, 300); // wait for the user to pause typing
});

async function geocode(q, signal) {
  const url = new URL('https://api.tmaps.tn/geocoding/forward');
  url.search = new URLSearchParams({
    q,
    country: 'TN',                 // Tunisia only
    limit: 5,
    lang: languageSelect.value,    // fr, en or ar
    proximity: '36.8065,10.1815',  // favour results near the user or the map center
    api_key: API_KEY,
  });
  const res = await fetch(url, { signal });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`${res.status}: ${data.error ?? res.statusText}`);
  return data.results;
}
  1. Debounce for 300 ms, so you send one request when the user pauses instead of one per letter.
  2. Use an AbortController. When a new search starts, cancel the previous one, otherwise a slow old response can land after a newer one and overwrite it.
  3. Pass proximity. Tunisia has a Rue de Marseille in a lot of cities, and biasing towards the user's position (or the map center) puts the right one at the top.

Each result gives you the point in geometry.location (lat, lng), a formatted address, a confidence score, and components with the commune, delegation and governorate.

Step 2: Arabic results, right to left

Set lang=ar and the whole address comes back in Arabic:

{
  "formatted": "نهج مرسيليا, حبيب ثامر, باب بحر, تونس, تونس",
  "type": "street",
  "confidence": 0.8
}

Arabic has to be displayed right to left. Since a suggestion list often mixes scripts (an Arabic address next to a French commune name, a road number in Latin digits…), don't hard-code the direction. Let the browser figure it out for each element:

const li = document.createElement('li');
li.dir = 'auto'; // the first strong character decides: Arabic means RTL, Latin means LTR
li.textContent = result.formatted;
<input id="address" type="search" dir="auto">

Putting dir="auto" on the input as well flips the caret and the alignment as soon as someone starts typing in Arabic. Without it, punctuation and numbers end up on the wrong side.

Step 3: what the API already understands

We ran a batch of real Tunisian queries against the API (lang=fr, country=TN). Here's the first result for each:

Query First result
نهج مرسيليا ✅ Rue de Marseille
nahj marseille ✅ Rue de Marseille, Tunis
9asr said ✅ Ksar Said, Le Bardo
7ammamet ✅ Hammamet
5ayreddine ✅ Rue Khayreddine
3in zaghouan ❌ no result
zan9et el bacha ⚠️ Khaireddine Pacha (not an impasse)

So Arabic script, nahj, and the digits 7, 9 and 5 already work. The digit 3 and some dialect words need a hand, and that help can live on the client.

Step 4: normalize Arabizi on the client

A small function rewrites dialect words and Arabizi digits into the spelling used in addresses:

// Tunisian dialect words and Arabizi digits, rewritten as in addresses
const DIALECT = {
  nahj: 'rue', nhej: 'rue', zan9a: 'impasse', zan9et: 'impasse', zanket: 'impasse',
  chare3: 'avenue', chara3: 'avenue', hay: 'cité', '7ay': 'cité',
};
const ARABIZI = { 3: 'a', 5: 'kh', 7: 'h', 9: 'k' };

function normalizeTunisian(text) {
  return text.toLowerCase().split(/\s+/).map((word) => {
    if (DIALECT[word]) return DIALECT[word];
    // Leave numbers (4014) and road names (rn9, c27) alone
    if (!/[a-z]/.test(word) || /^[a-z]{1,2}\d+$/.test(word)) return word;
    return word.replace(/3a/g, 'a').replace(/[3579]/g, (digit) => ARABIZI[digit]);
  }).join(' ');
}
"3in zaghouan"     → "ain zaghouan"
"nahj 5ayreddine"  → "rue khayreddine"
"zan9et el bacha"  → "impasse el bacha"
"chare3 bourguiba" → "avenue bourguiba"
"Rue 4014"         → "rue 4014"     (unchanged)
"RN9"              → "rn9"          (unchanged)
"نهج مرسيليا"       → "نهج مرسيليا"  (unchanged)

Don't skip the two guards. Tunisian addresses are full of numbers (Rue 4014) and road codes (RN9, C27), and rewriting those digits would break queries that worked perfectly well.

Step 5: search both versions in parallel

Normalizing can also make things worse: 9asr said already works as typed. So rather than replacing what the user wrote, search both versions and merge the results:

async function search() {
  const q = input.value.trim();
  if (q.length < 3) return;

  controller?.abort();
  controller = new AbortController();

  const normalized = normalizeTunisian(q);
  const queries = normalized === q.toLowerCase() ? [q] : [q, normalized];

  const lists = await Promise.all(queries.map((text) => geocode(text, controller.signal)));

  // Normalized results first, then the raw ones, without duplicates
  const seen = new Set();
  const results = lists.reverse().flat()
    .filter((r) => !seen.has(r.formatted) && seen.add(r.formatted))
    .slice(0, 5);

  render(results);
}

Plain French or Arabic text comes out of normalizeTunisian unchanged, so it still costs a single request. Only Arabizi or dialect input triggers the second one.

The difference shows right away: 3in zaghouan goes from nothing at all to Rue Ain Zaghouane and the Ain villages around Zaghouan.

Arabizi query "3in zaghouan" returning results

And 9asr said lands on Ksar Said, in Le Bardo:

Arabizi query "9asr said" returning Ksar Said

Step 6: don't trust every match

For a delivery address, a wrong match costs more than no match at all. The confidence score helps you decide:

  • at 0.85 or above, save the coordinates;
  • below that, keep the suggestion but ask the customer to check the pin on a map.

The checkout form demo shows the whole flow: pick a suggestion, the commune, delegation and governorate fill themselves in, then the customer can drag the pin, which is refined with reverse geocoding.

Known limits today

To be upfront about what this approach doesn't solve yet:

  • Some city names typed in Arabic script can come back as a different place with a similar name. If your users type city names in Arabic, keep proximity set and show the commune and governorate in each suggestion so they can pick the right one.
  • When several places share a name in different regions (there's more than one El Aouina), the ranking depends a lot on proximity. Send the user's real position whenever you have it.

To sum up

  1. Debounce, cancel stale requests, and bias results with proximity.
  2. Use lang=ar for Arabic results, and dir="auto" wherever you display them.
  3. Normalize dialect and Arabizi on the client, without touching numbers and road codes.
  4. Search the raw and normalized text in parallel, then merge and dedupe.
  5. Only save coordinates when confidence is high enough.

Going further

How do you type addresses? Share your trickiest Tunisian query in the comments, or write to us at [email protected].

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.