Collecting a delivery address is one thing. Getting the exact location is another. Users often know their street but struggle to describe where the entrance is - especially in large buildings, gated communities, or areas with poor address coverage.
In this tutorial, we'll build a delivery-friendly location picker that combines address autocomplete with an interactive map. The workflow is simple: users search for their address, then confirm or adjust the location by dragging a pin on the map. Reverse geocoding keeps the address text in sync whenever the pin moves.
Try the live demo:
β‘οΈ View on CodePen
APIs used:
- Address Autocomplete API - real-time suggestions as users type
- Reverse Geocoding API - convert coordinates back to addresses
- IP Geolocation API - detect approximate location without permission
- Map Tiles - raster map tiles for Leaflet
- Map Marker Icon API - custom marker icons
π§ Table of Contents
- Address Autocomplete Field
- Map Confirmation with Draggable Pin
- Drag-to-Adjust with Reverse Geocoding
- UX Improvements
- Confirm and Save
- Explore the Demo
- Summary
- FAQ
Step 1: Address Autocomplete Field
The address autocomplete field is the primary way users enter their location. As they type, the Geoapify Geocoder Autocomplete widget shows matching suggestions in real time.
Include the autocomplete library
<!-- Geoapify Geocoder Autocomplete CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@geoapify/geocoder-autocomplete@3.0.1/styles/minimal.css" />
<!-- Geoapify Geocoder Autocomplete JS -->
<script src="https://cdn.jsdelivr.net/npm/@geoapify/geocoder-autocomplete@3.0.1/dist/index.min.js"></script>
Create the autocomplete container
<div id="autocomplete" class="autocomplete-container"></div>
Initialize the autocomplete widget
const myAPIKey = "YOUR_API_KEY";
const ac = new autocomplete.GeocoderAutocomplete(
document.getElementById("autocomplete"),
myAPIKey,
{
skipIcons: true,
allowNonVerifiedStreet: true,
allowNonVerifiedHouseNumber: true
}
);
π API Key: Please sign up at geoapify.com and generate your own API key.
Options explained:
-
skipIcons: true- cleaner, text-only dropdown without location type icons -
allowNonVerifiedStreet- include addresses even in areas with incomplete street data -
allowNonVerifiedHouseNumber- accept house numbers not verified in the database
These options are useful for delivery applications where users may need to enter addresses in areas with limited coverage.
Handle the selection event
When the user selects an address, we store the result and prepare for the next step (map confirmation):
let lastAddress = null;
let lastLocation = null;
ac.on("select", (res) => {
if (!res || !res.properties) return;
const p = res.properties;
// Store the selected address and coordinates
lastAddress = p;
lastLocation = { lat: p.lat, lon: p.lon };
// The next step: place the pin on the map (see Step 2)
});
The res.properties object contains everything you need:
-
Coordinates:
lat,lon -
Formatted address:
formatted -
Components:
street,housenumber,city,postcode,country, etc.
π More options: See the Geocoder Autocomplete documentation for filters, language settings, and styling themes.
The autocomplete dropdown shows matching addresses. The user selects one, and we move to map confirmation.
Step 2: Map Confirmation with Draggable Pin
After the user selects an address, we show them where it is on a map. The pin is draggable so they can adjust the exact location - critical for large buildings, shared driveways, or places where the geocoded point isn't quite right.
Set up the Leaflet map
First, include Leaflet and create a map with Geoapify tiles:
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<div id="map"></div>
#map {
width: 100%;
height: 420px;
border-radius: 10px;
border: 1px solid #e6ecf5;
}
const map = L.map("map", { zoomControl: true }).setView([20, 0], 2);
// Retina displays need higher resolution tiles
const isRetina = L.Browser.retina;
const tileUrl = isRetina
? `https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}@2x.png?apiKey=${myAPIKey}`
: `https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}.png?apiKey=${myAPIKey}`;
L.tileLayer(tileUrl, {
attribution:
'Powered by <a href="https://www.geoapify.com/" target="_blank">Geoapify</a> | <a href="https://openmaptiles.org/" rel="nofollow" target="_blank">Β© OpenMapTiles</a> <a href="https://www.openstreetmap.org/copyright" rel="nofollow" target="_blank">Β© OpenStreetMap</a> contributors'
}).addTo(map);
The @2x suffix requests double-resolution tiles for retina screens. Leaflet's L.Browser.retina flag detects this automatically.
π Map styles: See the Map Tiles documentation for available styles like
osm-bright,osm-carto,positron, anddark-matter.
Create a custom marker icon
We use the Geoapify Marker Icon API to create a pin that matches your app's style:
const markerIcon = L.icon({
iconUrl: `https://api.geoapify.com/v1/icon/?type=awesome&color=%232ea2ff&size=large&scaleFactor=2&apiKey=${myAPIKey}`,
iconSize: [38, 56],
iconAnchor: [19, 51],
popupAnchor: [0, -60]
});
-
iconAnchor- defines which point of the icon sits on the coordinates (bottom tip of the pin) -
scaleFactor=2- renders a sharp image for retina displays
Place the marker when an address is selected
Extend the select handler from Step 1 to place (or move) the marker:
let marker = null;
ac.on("select", (res) => {
if (!res || !res.properties) return;
const p = res.properties;
lastAddress = p;
lastLocation = { lat: p.lat, lon: p.lon };
// Place or move the marker
const latlng = [p.lat, p.lon];
if (!marker) {
marker = L.marker(latlng, { icon: markerIcon, draggable: true }).addTo(map);
marker.on("dragend", onMarkerDragEnd); // Handle drag (Step 3)
} else {
marker.setLatLng(latlng);
}
// Zoom to street level
map.setView(latlng, Math.max(map.getZoom(), 16));
updateConfirmState();
});
Key points:
-
draggable: true- allows the user to adjust the pin position -
Math.max(map.getZoom(), 16)- zooms in to at least street level, but doesn't zoom out if user already zoomed in further - The marker is created lazily (only after the first selection)
After selecting an address, the map zooms in and shows a draggable pin. The user can now verify or adjust the location.
Step 3: Drag-to-Adjust with Reverse Geocoding
When the user drags the pin, we need to update the address to match the new location. This is where reverse geocoding comes in - converting coordinates back to a human-readable address.
Handle the drag end event
const MAX_LOCATION_TO_ADDRESS_ERROR = 50; // 50 meters
function onMarkerDragEnd() {
if (!marker) return;
const { lat, lng } = marker.getLatLng();
// Update the stored location
lastLocation = { lat, lon: lng };
// Show feedback
confirmMessage.textContent = "Pin updated. Please confirm the location.";
// Reverse geocode the new position
reverseGeocode(lat, lng, (rev) => {
const pr = rev.properties || {};
// Only update the address if the pin is close to a known address
if (pr.distance <= MAX_LOCATION_TO_ADDRESS_ERROR) {
lastAddress = pr;
ac.setValue(pr.formatted);
}
updateConfirmState();
});
}
The distance threshold
The distance field in the reverse geocoding response tells us how far the returned address is from the queried coordinates. We use a 50-meter threshold:
- Within 50m - update the autocomplete field with the new address
- Beyond 50m - keep the previous address text but save the new coordinates
This prevents confusing UX where dragging the pin a few meters causes the address to jump to a different street or building.
Reverse geocoding function
function reverseGeocode(lat, lon, callback) {
const url = `https://api.geoapify.com/v1/geocode/reverse?lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}&apiKey=${myAPIKey}`;
fetch(url)
.then((r) => r.json())
.then((data) => {
const feature = data && data.features && data.features[0] ? data.features[0] : null;
if (!feature) {
callback({ properties: {} });
return;
}
callback(feature);
})
.catch((err) => {
console.error("Reverse geocoding failed:", err);
callback({ properties: {} });
});
}
The Reverse Geocoding API returns the nearest address with:
-
formatted- full address string -
street,housenumber,city,postcode,country- individual components -
distance- meters from the queried point to the address
π Learn more: See Wikipedia: Reverse geocoding for background on how coordinate-to-address conversion works.
When the user drags the pin, reverse geocoding updates the address - but only if the new position is close enough to a valid address.
Step 4: UX Improvements
The basic workflow is complete: search, confirm, adjust. Now let's add features that make the experience smoother.
IP Geolocation for Initial Map View and Autocomplete Bias
Before the user types anything, we can detect their approximate location using IP Geolocation. This works without asking for permission - it's based on the user's IP address, not GPS.
fetch(`https://api.geoapify.com/v1/ipinfo?apiKey=${myAPIKey}`)
.then((r) => r.json())
.then((ip) => {
const loc = ip.location && ip.location.latitude && ip.location.longitude
? { lat: ip.location.latitude, lon: ip.location.longitude }
: null;
if (loc) {
// Center the map on the user's approximate location
map.setView([loc.lat, loc.lon], 12);
// Bias autocomplete suggestions toward this location
if (ac.addBiasByProximity) {
ac.addBiasByProximity({ lat: loc.lat, lon: loc.lon });
}
}
})
.catch(() => {
console.log("IP geolocation unavailable.");
});
Two benefits:
- Better initial view - the map shows the user's region instead of a world view
- More relevant suggestions - autocomplete results are sorted by proximity (users in Berlin see Berlin addresses first)
This happens silently on page load. The user doesn't see a permission prompt, and they can still search for addresses anywhere in the world.
IP geolocation centers the map on the user's approximate location. Autocomplete suggestions are biased toward nearby addresses.
"Use My Location" Button
For users who want to skip typing, we provide a button that uses the browser's Geolocation API. This requires explicit permission but provides GPS-level accuracy.
<button id="geo-btn" type="button">Use my location</button>
geoBtn.addEventListener("click", () => {
if (!navigator.geolocation) {
alert("Geolocation is not supported by your browser.");
return;
}
geoBtn.disabled = true;
navigator.geolocation.getCurrentPosition(
(pos) => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
const latlng = [lat, lon];
// Place or move the marker
if (!marker) {
marker = L.marker(latlng, { icon: markerIcon, draggable: true }).addTo(map);
marker.on("dragend", onMarkerDragEnd);
} else {
marker.setLatLng(latlng);
}
map.setView(latlng, Math.max(map.getZoom(), 16));
lastLocation = { lat, lon };
// Reverse geocode to fill the address field
reverseGeocode(lat, lon, (rev) => {
const pr = rev.properties || {};
if (pr.formatted) {
ac.setValue(pr.formatted);
}
lastAddress = pr;
updateConfirmState();
});
geoBtn.disabled = false;
},
(err) => {
alert("Unable to retrieve your location.");
geoBtn.disabled = false;
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
});
How it works:
- Request the user's current position (triggers permission prompt)
- Place the marker at those coordinates
- Reverse geocode to get the address
- Fill the autocomplete field with the formatted address
The enableHighAccuracy: true option requests GPS-level precision on mobile devices.
After clicking "Use my location", the marker appears at the user's GPS position and the address field is filled automatically.
IP Geolocation vs. Browser Geolocation
| Feature | IP Geolocation | Browser Geolocation |
|---|---|---|
| Permission required | No | Yes |
| Accuracy | City level (~5-50 km) | GPS level (~5-50 m) |
| Works on desktop | Yes | Sometimes (depends on WiFi) |
| Use case | Initial bias, map centering | "Use my location" button |
Use both: IP geolocation for the initial experience, browser geolocation when the user explicitly requests it.
Step 5: Confirm and Save
The final step collects the pin coordinates and address for your backend. The confirm button is only enabled after a location is set.
Enable/disable the confirm button
function updateConfirmState() {
const hasPin = !!marker;
confirmBtn.disabled = !hasPin;
}
Handle confirmation
confirmBtn.addEventListener("click", () => {
if (!lastLocation || !lastAddress) return;
const lat = lastLocation.lat;
const lng = lastLocation.lon;
const addr = lastAddress.formatted;
confirmMessage.textContent = addr
? `Saved: ${lat.toFixed(6)}, ${lng.toFixed(6)} - ${addr}`
: `Saved: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
// Here you would send the data to your backend:
// { lat, lon: lng, address: addr, addressComponents: lastAddress }
});
What to save:
-
Coordinates (
lat,lon) - essential for mapping, routing, and delivery logistics - Formatted address - for display to users and drivers
- Address components - for validation, sorting, or analytics
Calculate distance between address and pin
If the user dragged the pin, the final coordinates may differ from the geocoded address. You can calculate this distance using the Haversine formula:
function haversineMeters(lat1, lon1, lat2, lon2) {
const toRad = (d) => (d * Math.PI) / 180;
const R = 6371000; // Earth's radius in meters
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
// Usage: const dist = haversineMeters(lastAddress.lat, lastAddress.lon, lat, lng);
This helps you understand how much the user adjusted the location - useful for analytics or quality checks.
After confirmation, the saved coordinates and address are displayed. This data is ready to send to your backend.
Step 6: Explore the Demo
The live CodePen demo ties everything together into a working example you can fork and customize.
What the demo shows
- Search - Type in the autocomplete field and select an address
- Confirm - Pin appears on the map at the selected location
- Adjust - Drag the pin to fine-tune the exact spot
- Save - Click confirm to see the final coordinates and address
UX details worth noting
- IP geolocation - Map centers on user's location without permission prompt
- Retina support - Sharp tiles and markers on high-DPI displays
- Theme selector - Light and dark modes available
- Developer panels - Shows raw API responses for debugging
Where to use this pattern
This workflow fits naturally into:
- Delivery checkout - let customers pinpoint their exact entrance or gate
- Pickup points - help users select where to meet a driver
- User onboarding - collect verified addresses during account creation
- Real estate - let users mark property locations on a map
- Field service - capture job site locations with precision
π Try it in the interactive demo:
Summary
We've built a location picker with a two-step workflow:
- Address autocomplete - fast search with real-time suggestions (primary input)
- Map confirmation - visual verification with a draggable pin (fine-tuning)
Key features:
- Reverse geocoding keeps the address in sync when the pin moves
- IP geolocation provides a better initial experience without permission prompts
- Browser geolocation offers GPS-level accuracy when requested
- A distance threshold prevents confusing address jumps for small adjustments
Useful links:
- Geoapify Address Autocomplete
- Reverse Geocoding API
- IP Geolocation API
- Map Tiles
- Marker Icon API
- Geocoder Autocomplete on npm
- Leaflet documentation
- OpenStreetMap attribution
FAQ
Q: What's the difference between IP geolocation and browser geolocation?
A: IP geolocation detects approximate location (city level) without user permission. Browser geolocation requests explicit permission and provides GPS-level accuracy on mobile devices. Use IP geolocation for initial bias; use browser geolocation for the "Use my location" button.
Q: Why use a distance threshold when dragging the pin?
A: The threshold (50 meters in our demo) prevents confusing UX. Without it, dragging the pin a few meters might cause the address field to jump to a different street or building. The threshold ensures we only update the address for significant moves.
Q: How accurate is reverse geocoding?
A: It depends on data coverage. In most urban areas, reverse geocoding returns building-level addresses. In rural areas with limited data, it may return only the nearest street or locality.
Q: Can I use this with React, Vue, or Angular?
A: Yes. The Geocoder Autocomplete library works with any framework. There are also dedicated packages for React and Angular.
Q: How do I customize the autocomplete dropdown styling?
A: The library includes themes (minimal, minimal-dark, round-borders, round-borders-dark). You can also override CSS variables or write custom styles.
Q: What data should I save to my backend?
A: Save both the coordinates (lat/lon) and the structured address. Coordinates are essential for mapping and routing; the address is useful for display and verification.
Q: How do I handle users who deny geolocation permission?
A: Fall back to the address search or IP geolocation. The demo already handles this - if geolocation fails, an alert is shown and the button re-enables.
Try It Now
π Open the Live Demo
Please sign up at geoapify.com and generate your own API key to start building location pickers for your delivery, pickup, and onboarding flows.






Top comments (0)