Autocomplétion des adresses d'entreprises avec les données KVK
Créez une autocomplétion d'adresses d'entreprises avec les données KVK : recherche avec anti-rebond, liste déroulante, remplissage des champs et codes postaux.
Les formulaires d’adresse sont fastidieux. Pour les applications B2B, c’est encore pire : vos utilisateurs doivent saisir un nom d’entreprise, retrouver la bonne adresse officielle et s’assurer que tout correspond aux registres. Une autocomplétion d’adresses d’entreprises supprime l’essentiel de cette friction.
Dans ce guide, nous construisons une autocomplétion qui recherche des entreprises néerlandaises par nom et remplit l’adresse complète à partir des données KVK.
L’approche
L’autocomplétion fonctionne en trois étapes :
- L’utilisateur commence à saisir un nom d’entreprise
- Une requête de recherche avec anti-rebond interroge l’API de données KVK
- Les résultats apparaissent dans une liste déroulante ; en sélectionner un remplit les champs d’adresse
Construisons chaque élément.
Étape 1 : la structure HTML
Commencez par une mise en page de formulaire propre :
<form id="business-form">
<div class="autocomplete-wrapper">
<label for="company-search">Company name</label>
<input
type="text"
id="company-search"
placeholder="Start typing a company name..."
autocomplete="off"
role="combobox"
aria-expanded="false"
aria-controls="search-results"
aria-autocomplete="list"
>
<ul id="search-results" role="listbox" class="results-dropdown hidden"></ul>
</div>
<div class="address-fields">
<div class="field-row">
<div class="field">
<label for="kvk-number">KVK number</label>
<input type="text" id="kvk-number" readonly>
</div>
</div>
<div class="field-row">
<div class="field">
<label for="street">Street</label>
<input type="text" id="street" readonly>
</div>
<div class="field field-small">
<label for="house-number">Number</label>
<input type="text" id="house-number" readonly>
</div>
</div>
<div class="field-row">
<div class="field field-small">
<label for="postal-code">Postal code</label>
<input type="text" id="postal-code" readonly>
</div>
<div class="field">
<label for="city">City</label>
<input type="text" id="city" readonly>
</div>
</div>
</div>
</form>
Étape 2 : la recherche avec anti-rebond
Vous ne voulez pas envoyer une requête d’API à chaque frappe. Une fonction d’anti-rebond (debounce) attend que l’utilisateur marque une pause dans sa saisie :
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const searchInput = document.getElementById('company-search');
const resultsList = document.getElementById('search-results');
const handleSearch = debounce(async (query) => {
if (query.length < 2) {
hideResults();
return;
}
try {
const companies = await searchCompanies(query);
renderResults(companies);
} catch (error) {
console.error('Search failed:', error);
hideResults();
}
}, 300);
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value.trim());
});
La fonction searchCompanies appelle votre backend, qui relaie la requête vers l’API KVKBase :
async function searchCompanies(query) {
const response = await fetch(
`/api/companies/search?q=${encodeURIComponent(query)}&limit=8`
);
if (!response.ok) {
throw new Error(`Search error: ${response.status}`);
}
return response.json();
}
Côté serveur :
app.get('/api/companies/search', async (req, res) => {
const { q, limit = 8 } = req.query;
if (!q || q.length < 2) {
return res.json([]);
}
const response = await fetch(
`https://api.kvkbase.nl/api/v1/search?q=${encodeURIComponent(q)}&limit=${limit}`,
{ headers: { 'Authorization': `Bearer ${process.env.KVKBASE_API_KEY}` } }
);
const data = await response.json();
res.json(data);
});
Étape 3 : la liste déroulante
Affichez les résultats de recherche dans une liste déroulante soignée :
function renderResults(companies) {
if (companies.length === 0) {
resultsList.innerHTML = '<li class="no-results">No companies found</li>';
showResults();
return;
}
resultsList.innerHTML = companies.map((company, index) => `
<li
role="option"
id="result-${index}"
class="result-item"
data-kvk="${company.kvkNumber}"
tabindex="-1"
>
<span class="company-name">${escapeHtml(company.tradeName)}</span>
<span class="company-details">
${escapeHtml(company.address?.city || '')} - KVK ${company.kvkNumber}
</span>
</li>
`).join('');
// Associer les gestionnaires de clic
resultsList.querySelectorAll('.result-item').forEach(item => {
item.addEventListener('click', () => {
const kvkNumber = item.dataset.kvk;
selectCompany(companies.find(c => c.kvkNumber === kvkNumber));
});
});
showResults();
}
function showResults() {
resultsList.classList.remove('hidden');
searchInput.setAttribute('aria-expanded', 'true');
}
function hideResults() {
resultsList.classList.add('hidden');
searchInput.setAttribute('aria-expanded', 'false');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
Étape 4 : sélectionner et remplir les champs d’adresse
Quand l’utilisateur sélectionne une entreprise, remplissez tous les champs d’adresse :
async function selectCompany(company) {
// Mettre à jour le champ de recherche avec le nom de l'entreprise
searchInput.value = company.tradeName;
hideResults();
// Remplir les champs d'adresse
document.getElementById('kvk-number').value = company.kvkNumber;
document.getElementById('street').value = company.address?.street || '';
document.getElementById('house-number').value = company.address?.houseNumber || '';
document.getElementById('postal-code').value = company.address?.postalCode || '';
document.getElementById('city').value = company.address?.city || '';
// Retirer readonly pour que l'utilisateur puisse modifier si besoin
document.querySelectorAll('.address-fields input[readonly]').forEach(input => {
input.removeAttribute('readonly');
});
}
Étape 5 : navigation au clavier
Une bonne autocomplétion prend en charge la navigation complète au clavier :
let activeIndex = -1;
searchInput.addEventListener('keydown', (e) => {
const items = resultsList.querySelectorAll('.result-item');
if (items.length === 0) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
activeIndex = Math.min(activeIndex + 1, items.length - 1);
updateActiveItem(items);
break;
case 'ArrowUp':
e.preventDefault();
activeIndex = Math.max(activeIndex - 1, 0);
updateActiveItem(items);
break;
case 'Enter':
e.preventDefault();
if (activeIndex >= 0 && items[activeIndex]) {
items[activeIndex].click();
}
break;
case 'Escape':
hideResults();
activeIndex = -1;
break;
}
});
function updateActiveItem(items) {
items.forEach((item, i) => {
if (i === activeIndex) {
item.classList.add('active');
item.scrollIntoView({ block: 'nearest' });
searchInput.setAttribute('aria-activedescendant', item.id);
} else {
item.classList.remove('active');
}
});
}
Étape 6 : la mise en forme
Un CSS propre pour l’autocomplétion :
.autocomplete-wrapper {
position: relative;
max-width: 500px;
}
.results-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin: 0;
padding: 0;
list-style: none;
background: white;
border: 1px solid #d1d5db;
border-top: none;
border-radius: 0 0 8px 8px;
max-height: 300px;
overflow-y: auto;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.results-dropdown.hidden {
display: none;
}
.result-item {
padding: 12px 16px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 2px;
border-bottom: 1px solid #f3f4f6;
}
.result-item:hover,
.result-item.active {
background: #f0f7ff;
}
.company-name {
font-weight: 500;
color: #111827;
}
.company-details {
font-size: 13px;
color: #6b7280;
}
.no-results {
padding: 12px 16px;
color: #6b7280;
font-style: italic;
}
.field-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.field {
flex: 1;
}
.field-small {
flex: 0 0 120px;
}
Combiner avec une API de codes postaux
Pour les adresses de particuliers, ou lorsque les utilisateurs veulent saisir une autre adresse que celle du registre, vous pouvez combiner les données KVK avec une API néerlandaise de codes postaux :
async function lookupByPostalCode(postalCode, houseNumber) {
const response = await fetch(
`/api/address?postalCode=${postalCode}&houseNumber=${houseNumber}`
);
return response.json();
}
// Laisser l'utilisateur basculer entre l'adresse KVK et la saisie manuelle
document.getElementById('manual-address-toggle').addEventListener('click', () => {
// Afficher les champs code postal + numéro
document.getElementById('manual-entry').classList.remove('hidden');
// Rendre les champs d'adresse modifiables
document.querySelectorAll('.address-fields input').forEach(input => {
input.removeAttribute('readonly');
input.value = '';
});
});
Cela donne de la souplesse aux utilisateurs : l’adresse KVK enregistrée dans la plupart des cas, ou une autre adresse de livraison ou de facturation lorsque c’est nécessaire.
Conseils de performance
Quelques optimisations pour garder l’autocomplétion rapide et efficace :
- Anti-rebond à 300 ms : assez rapide pour sembler réactif, assez lent pour éviter un excès d’appels d’API
- Minimum 2 caractères : ne cherchez pas sur une seule lettre, les résultats seraient trop larges
- Limiter les résultats à 8 : plus de résultats allongent la liste, qui devient plus difficile à parcourir
- Mettre en cache les recherches récentes : si l’utilisateur tape « Bol », efface, puis retape « Bol », servez la réponse depuis le cache
- Annuler les requêtes précédentes : si une nouvelle frappe arrive avant la fin de la requête précédente, annulez l’ancienne
let currentController = null;
async function searchCompanies(query) {
// Annuler la requête précédente
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
const response = await fetch(
`/api/companies/search?q=${encodeURIComponent(query)}&limit=8`,
{ signal: currentController.signal }
);
return response.json();
}
En résumé
Une autocomplétion d’adresses d’entreprises fondée sur les données KVK supprime la saisie manuelle, réduit les erreurs et donne à votre application un aspect soigné. Avec KVKBase, l’API de recherche fait le gros du travail : il ne vous reste qu’à construire la couche d’interface par-dessus.
La combinaison d’une recherche avec anti-rebond, d’une navigation au clavier et d’une liste déroulante bien stylée offre une expérience qui paraît native dans le navigateur, tout en s’appuyant sur une source de données faisant autorité.