Tunisian address search: French, Arabic and Arabizi

TMaps example: maps and geocoding API for Tunisia. Replace YOUR_API_KEY with your key from https://app.tmaps.tn. More: https://www.tmaps.tn

by Tmaps

HTML

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>

<div id="map"></div>
<div class="panel">
  <b>Address search for Tunisia</b><br>
  <small>Try <i>Rue de Marseille</i>, <i>نهج مرسيليا</i> or <i>9asr said</i></small>
  <form id="form">
    <input id="q" type="search" dir="auto" autocomplete="off" value="9asr said">
    <select id="lang"><option value="fr">FR</option><option value="en">EN</option><option value="ar">AR</option></select>
  </form>
  <ul id="results"></ul>
</div>

CSS

html, body { height: 100%; margin: 0; font-family: system-ui, sans-serif; }
#map { position: absolute; inset: 0; }
.panel {
  position: absolute; z-index: 1000; top: 12px; left: 12px; max-width: 320px;
  background: #fff; border-radius: 8px; padding: 10px 12px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, .2); font-size: 14px;
}
.panel a { color: #0b63f6; }
form { display: flex; gap: 6px; margin-top: 8px; }
#q { flex: 1; padding: 6px 8px; border: 1px solid #ccd; border-radius: 6px; }
#results { list-style: none; margin: 8px 0 0; padding: 0; max-height: 50vh; overflow: auto; }
#results li { padding: 6px; border-radius: 6px; cursor: pointer; }
#results li:hover, #results li.active { background: #eef3ff; }
#results small { color: #667; }

JavaScript

// TMaps: maps and geocoding API for Tunisia. https://www.tmaps.tn
// 1. Create a free API key on https://app.tmaps.tn
// 2. Add fiddle.jshell.net to its authorized domains
// 3. Paste it below
const API_KEY = 'YOUR_API_KEY';

const map = L.map('map').setView([36.8002, 10.1815], 12); // Tunis
L.tileLayer(`https://tiles.tmaps.tn/r-tile/{z}/{x}/{y}?api_key=${API_KEY}`, {
  maxZoom: 18,
  attribution: '&copy; <a href="https://www.tmaps.tn">TMaps</a>',
}).addTo(map);

const input = document.getElementById('q');
const list = document.getElementById('results');
const marker = L.marker([0, 0]);
let timer;
let controller;

// Tunisian dialect words and Arabizi digits, rewritten the way addresses are spelled
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 codes (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(' ');
}

async function geocode(q, signal) {
  const center = map.getCenter();
  const url = new URL('https://api.tmaps.tn/geocoding/forward');
  url.search = new URLSearchParams({
    q,
    country: 'TN',
    limit: 5,
    lang: document.getElementById('lang').value,
    proximity: `${center.lat.toFixed(5)},${center.lng.toFixed(5)}`,
    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;
}

// Search the raw input and its normalized version in parallel, then merge...