Blog · Entegrasyon ve mimari

Adres Otomatik Tamamlama: İyi Bir Deneyimin Ayrıntıları

Adres otomatik tamamlama kutusunu /v1/autocomplete ile kurun: minimum karakter, debounce, il/ilçe ile daraltma, seçim akışı ve klavye erişilebilirliği.

4 dk okuma

Adres otomatik tamamlama, kullanıcının birkaç harf yazarak il, ilçe, mahalle ya da cadde/sokak seçmesini sağlayan arama kutusudur. İlk bakışta basit görünür. Ancak bazı kutular her tuş vuruşunda istek atar, eski yanıtı yenisinin üstüne yazar ya da klavyeyle kullanılamaz. Böyle kutular hem sunucuyu hem kullanıcıyı yorar. Bu yazıda /v1/autocomplete ucuyla hızlı, yazım hatalarına toleranslı ve erişilebilir bir kutunun nasıl kurulacağı adım adım ele alınıyor.

Adres otomatik tamamlama için mimari: anahtar sunucuda kalır

X-API-Key başlığı tarayıcı koduna konmamalıdır. Bu nedenle arama kutusu API'ye doğrudan istek atmaz. İstek, kendi sunucunuzdaki ince bir ara uca gider. Bu ara uç anahtarı ekler ve isteği iletir. Anahtar yönetiminin ayrıntıları için API anahtarını güvenli kullanma rehberine bakabilirsiniz.

Önce ucu terminalden deneyin. Sorgudaki yazım hatası bilinçli olarak bırakılmıştır:

curl --get "https://api.apiturkiye.com/v1/autocomplete" \
  --data-urlencode "q=Bağdat Cadessi" \
  --data-urlencode "provinceCode=34" \
  --data-urlencode "types=street" \
  --data-urlencode "limit=5" \
  -H "X-API-Key: apt_live_..."

Aşağıdaki Node.js ara ucu parametreleri doğrular ve yalnız bilinen alanları iletir. Ayrıca hata durumlarını arayüz için sadeleştirir:

// server.js — Node.js 18+ (yerleşik fetch); kurulum: npm install express
const express = require("express");

const app = express();
const TYPES = new Set(["all", "province", "district", "neighborhood", "street"]);

app.get("/api/adres-ara", async (req, res) => {
  const q = String(req.query.q ?? "").trim();
  if (q.length < 3 || q.length > 100) return res.json({ data: [] });

  const url = new URL("https://api.apiturkiye.com/v1/autocomplete");
  url.searchParams.set("q", q);
  url.searchParams.set("limit", "8");
  const types = String(req.query.types ?? "all");
  url.searchParams.set("types", TYPES.has(types) ? types : "all");
  for (const key of ["provinceCode", "districtCode", "neighborhoodCode"]) {
    const value = String(req.query[key] ?? "");
    if (/^\d+$/.test(value)) url.searchParams.set(key, value);
  }

  try {
    const upstream = await fetch(url, {
      headers: { "X-API-Key": process.env.APITURKIYE_KEY },
    });
    if (upstream.status === 400) return res.json({ data: [] }); // ör. invalid_query
    if (upstream.status === 429) {
      const retryAfter = upstream.headers.get("Retry-After");
      if (retryAfter) res.set("Retry-After", retryAfter);
      return res.status(429).json({ data: [] });
    }
    if (!upstream.ok) return res.status(502).json({ data: [] });
    const { data } = await upstream.json();
    res.json({ data });
  } catch {
    res.status(502).json({ data: [] });
  }
});

app.listen(3000);

401 ve 403 yanıtlarını sunucu loglarınızda izleyin. Bu durumları otomatik yeniden denemeyle çözmeye çalışmayın, çünkü sorun anahtarda, pakette veya yetkidedir.

Minimum karakter ve debounce

q parametresi 3–100 karakter kabul eder. Normalize edilmiş sorgunun da en az 3 karakter olması gerekir. İstemcide kırpılmış metnin uzunluğunu kontrol etmek gereksiz istekleri büyük ölçüde önler. Yine de "a.." gibi bir girdi normalize edildikten sonra kısa kalabilir. Yukarıdaki ara uç bu yüzden 400 yanıtını boş liste olarak döndürür.

Debounce, kullanıcı yazmaya ara verene kadar beklemek demektir. 250 ms civarı bir gecikmeyle başlayıp gerçek kullanımı gözleyerek ayarlayabilirsiniz. Debounce tek başına yetmez. Yavaş dönen eski bir yanıt, yeni yanıtın üstüne yazılabilir. Bunu önlemek için önceki isteği AbortController ile iptal edin ve yanıtın hâlâ güncel sorguya ait olduğunu doğrulayın.

const input = document.getElementById("adres-ara");
const list = document.getElementById("adres-oneriler");
const status = document.getElementById("adres-durum");

const MIN_CHARS = 3;
const DEBOUNCE_MS = 250;
const scope = {}; // provinceCode, districtCode, neighborhoodCode
let timer, controller;
let results = [];
let active = -1;
let selected = null;

input.addEventListener("input", () => {
  clearTimeout(timer);
  selected = null; // metin değişti; önceki seçim artık geçerli değil
  const q = input.value.trim();
  if (q.length < MIN_CHARS) return render([]);
  timer = setTimeout(() => search(q), DEBOUNCE_MS);
});

async function search(q) {
  controller?.abort();
  controller = new AbortController();
  const params = new URLSearchParams({ q, ...scope });
  try {
    const res = await fetch(`/api/adres-ara?${params}`, { signal: controller.signal });
    const { data } = res.ok ? await res.json() : { data: [] };
    if (input.value.trim() === q) render(data, q);
  } catch (err) {
    if (err.name !== "AbortError") render([]);
  }
}

Bir uyarı daha: Türkçe karakterleri istemcide dönüştürmeyin. Uç, "bagdat" ile "Bağdat" arasındaki farka ve küçük yazım hatalarına zaten toleranslıdır. Kullanıcının yazdığını olduğu gibi gönderin.

Adres aramasını il ve ilçeyle daraltmak

provinceCode, districtCode ve neighborhoodCode parametreleri aramayı seçilen coğrafi kapsamla sınırlar. types parametresi de dönen sonuç türlerini belirler. Kapsamı daraltmak hem öneri listesini kısaltır hem de farklı illerdeki aynı adlı yerlerin karışmasını önler. Kapsam şu yollarla belirlenebilir:

  • Önce il seçtirmek: Formda il seçimi ayrı bir alandaysa, seçilen ilin kodunu provinceCode olarak gönderin. Kademeli seçim yapısı için il, ilçe ve mahalle seçimli adres formu yazısına bakabilirsiniz.
  • Tek şehirde hizmet vermek: Yalnız bir ilde teslimat yapıyorsanız provinceCode değerini sunucuda sabitleyin.
  • Alan bazlı kutular: Ayrı bir "Cadde/Sokak" alanında types=street ve seçili mahallenin neighborhoodCode değerini kullanın.

İki kurala dikkat edin. Birincisi, yalnız API yanıtlarından aldığınız kodları gönderin. İkincisi, üst kapsam değiştiğinde alt kodları temizleyin. İl değiştiği hâlde eski districtCode gönderilirse sonuç listesi boş döner. Kullanıcı bunu "adres bulunamadı" diye yorumlar. Aktif kapsamı arayüzde görünür kılın ("Kadıköy, İstanbul içinde aranıyor" gibi) ve tek dokunuşla kaldırılabilir yapın.

Sonuç türleri ve seçim sonrası akış

Her öneri bir type taşır: province, district, neighborhood veya street. Sonuç türüne göre district ve neighborhood alanları null olabilir. Seçimden sonraki adım bu türe göre değişir:

TürKapsama eklenenSonraki adım
provinceprovinceCodeİlçe veya mahalle araması
districtprovinceCode, districtCodeMahalle veya sokak araması
neighborhoodüç kodun tamamıtypes=street ile sokak araması
streetüç kodun tamamıDış kapı numarası adımı
function choose(i) {
  const item = results[i];
  render([]);
  input.value = item.label;
  selected = { type: item.type, stableId: item.stableId, code: item.code };
  const own = (type, parent) => (item.type === type ? item.code : parent?.code);
  setScope("provinceCode", own("province", item.province));
  setScope("districtCode", own("district", item.district));
  setScope("neighborhoodCode", own("neighborhood", item.neighborhood));
  if (item.type === "street") openDoorStep(item.code); // kendi kapı alanınızı açar
}

function setScope(key, value) {
  if (value) scope[key] = value;
  else delete scope[key];
}

Kalıcı referans olarak stableId değerini saklayın. Kullanıcı seçimden sonra metni değiştirirse eski seçimi silin; yukarıdaki input dinleyicisi selected = null ile bunu yapar. Aksi hâlde ekranda başka bir adres yazarken arka planda eski kimlik gönderilir.

Autocomplete dış kapı araması yapmaz. Sokak seçildikten sonra kapı numarasını o sokak kapsamında, sunucu tarafından arayın:

import os
import requests

def kapi_ara(street_code: str, door_prefix: str) -> list[dict]:
    res = requests.get(
        f"https://api.apiturkiye.com/v1/streets/{street_code}/buildings",
        params={"q": door_prefix, "limit": 20},
        headers={"X-API-Key": os.environ["APITURKIYE_KEY"]},
        timeout=10,
    )
    res.raise_for_status()
    return [
        {"stableId": d["stableId"], "id": d["id"], "doorNumber": d["doorNumber"]}
        for d in res.json()["data"]
    ]

Daha fazla sonuç gerekirse meta.nextCursor ile sonraki sayfayı alabilirsiniz. Aynı kapı numarasında birden fazla kayıt olabilir. Bu yüzden kullanıcının seçtiği kaydın id değerini de saklayın.

score alanı yalnız sıralama içindir; adresin geçerli olduğunu kanıtlamaz. Siparişi kaydetmeden önce kesin kontrol istiyorsanız, Pro ve üzeri paketlerde bulunan POST /v1/addresses/validate ucunu kullanabilirsiniz. Bu uçta isteğe bağlı doorId alanına sakladığınız id değerini gönderin.

Klavye ile gezinme ve erişilebilirlik

İyi bir autocomplete, fare olmadan da eksiksiz kullanılabilmelidir. Kullanıcılar şu davranışları bekler:

  • Aşağı/Yukarı ok: Öneriler arasında döngüsel gezinme.
  • Enter: Etkin öneriyi seçme.
  • Escape: Listeyi kapatma.
  • Ekran okuyucu: Kaç öneri listelendiğini ya da hiç sonuç olmadığını duyurma.

ARIA combobox deseni bu davranışları yardımcı teknolojilere aktarır. Aşağıdaki JavaScript kodu öznitelikleri ayarlar, listeyi çizer ve klavyeyi yönetir:

const TYPE_LABELS = { province: "İl", district: "İlçe", neighborhood: "Mahalle", street: "Cadde/Sokak" };

input.setAttribute("role", "combobox");
input.setAttribute("aria-autocomplete", "list");
input.setAttribute("aria-controls", list.id);
input.setAttribute("aria-expanded", "false");
input.setAttribute("autocomplete", "off");
list.setAttribute("role", "listbox");
status.setAttribute("aria-live", "polite");

function render(items, query = "") {
  results = items;
  active = -1;
  list.replaceChildren(...items.map((item, i) => {
    const li = document.createElement("li");
    li.id = `adres-oneri-${i}`;
    li.setAttribute("role", "option");
    li.setAttribute("aria-selected", "false");
    li.textContent = `${item.label} · ${TYPE_LABELS[item.type]}`;
    li.addEventListener("mousedown", (e) => { e.preventDefault(); choose(i); });
    return li;
  }));
  input.setAttribute("aria-expanded", String(items.length > 0));
  input.removeAttribute("aria-activedescendant");
  status.textContent = items.length
    ? `${items.length} öneri listelendi.`
    : query ? "Eşleşen adres bulunamadı." : "";
}

function setActive(i) {
  list.children[active]?.setAttribute("aria-selected", "false");
  active = i;
  const el = list.children[i];
  el.setAttribute("aria-selected", "true");
  el.scrollIntoView({ block: "nearest" });
  input.setAttribute("aria-activedescendant", el.id);
}

input.addEventListener("keydown", (e) => {
  if (e.key === "Escape") return render([]);
  if (!results.length) return;
  if (e.key === "ArrowDown" || e.key === "ArrowUp") {
    e.preventDefault();
    const n = results.length;
    const step = e.key === "ArrowDown" ? 1 : -1;
    setActive(active < 0 ? (step > 0 ? 0 : n - 1) : (active + step + n) % n);
  } else if (e.key === "Enter" && active >= 0) {
    e.preventDefault();
    choose(active);
  }
});

input.addEventListener("blur", () => render([]));

Koddaki bazı ayrıntılar bilinçli tercihlerdir:

  • Odak kontrolü: Odak her zaman giriş kutusunda kalır; etkin öneri aria-activedescendant ile bildirilir.
  • mousedown ve preventDefault: Bu ikili, tıklama işlenmeden önce blur olayının listeyi kapatmasını önler.
  • autocomplete="off": Tarayıcının kendi önerileri sizin listenizin üstüne açılmaz.
  • Tür etiketi: Aynı adı taşıyan ilçe, mahalle ve sokak birbirinden ayrılabilir.

matchType değeri fuzzy olduğunda yazılan metin etikette birebir geçmeyebilir. Bu durumda alt dize vurgulaması yanlış harfleri boyar. Vurgulamayı yalnız exact, prefix ve word_prefix eşleşmelerinde kullanın. Etkin öğeyi yalnız renkle değil, belirgin bir çerçeve veya arka planla da gösterin.

Yayına almadan önce kısa kontrol listesi

  1. API anahtarı yalnız sunucuda mı? Ara uç, parametreleri doğruluyor mu?
  2. Minimum karakter, debounce ve eski isteği iptal etme birlikte çalışıyor mu?
  3. Üst kapsam değişince alt kodlar temizleniyor mu? Kapsam kullanıcıya görünüyor mu?
  4. Kalıcı referans olarak stableId saklanıyor mu? Metin değişince seçim sıfırlanıyor mu?
  5. 429 yanıtında Retry-After dikkate alınıyor mu? Arayüz boş listeyle sakin kalıyor mu?
  6. Kutu yalnız klavyeyle ve bir ekran okuyucuyla baştan sona test edildi mi?

Sonraki adım

Tüm parametreler ve yanıt alanları için API dokümantasyonunu inceleyebilirsiniz. Hazır örnekleri adres seçici bileşenleri sayfasında bulabilirsiniz. Akıllı adres arama Core paketinde de yer alır. Ücretsiz kayıt ile anahtarınızı alıp bu kodu kendi formunuzda deneyebilirsiniz.