DEV Community

Cover image for Leaflet Location Picker with Address Autocomplete, Geolocation, and Draggable Pin with Geoapify
Casey Rivers for Geoapify Maps API

Posted on

Leaflet Location Picker with Address Autocomplete, Geolocation, and Draggable Pin with Geoapify

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:


🧭 Table of Contents

  1. Address Autocomplete Field
  2. Map Confirmation with Draggable Pin
  3. Drag-to-Adjust with Reverse Geocoding
  4. UX Improvements
  5. Confirm and Save
  6. Explore the Demo
  7. Summary
  8. 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>
Enter fullscreen mode Exit fullscreen mode

Create the autocomplete container

<div id="autocomplete" class="autocomplete-container"></div>
Enter fullscreen mode Exit fullscreen mode

Initialize the autocomplete widget

const myAPIKey = "YOUR_API_KEY";

const ac = new autocomplete.GeocoderAutocomplete(
  document.getElementById("autocomplete"),
  myAPIKey,
  {
    skipIcons: true,
    allowNonVerifiedStreet: true,
    allowNonVerifiedHouseNumber: true
  }
);
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ 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)
});
Enter fullscreen mode Exit fullscreen mode

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.

Address autocomplete dropdown showing suggestions as the user types

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>
Enter fullscreen mode Exit fullscreen mode
<div id="map"></div>
Enter fullscreen mode Exit fullscreen mode
#map {
  width: 100%;
  height: 420px;
  border-radius: 10px;
  border: 1px solid #e6ecf5;
}
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode

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, and dark-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]
});
Enter fullscreen mode Exit fullscreen mode
  • 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();
});
Enter fullscreen mode Exit fullscreen mode

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)

Map showing a pin placed at the selected address location, zoomed to street level

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();
  });
}
Enter fullscreen mode Exit fullscreen mode

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: {} });
    });
}
Enter fullscreen mode Exit fullscreen mode

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.

Pin dragged to a new position with

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.");
  });
Enter fullscreen mode Exit fullscreen mode

Two benefits:

  1. Better initial view - the map shows the user's region instead of a world view
  2. 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.

Map centered on user's city (detected via IP) with autocomplete showing local suggestions

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>
Enter fullscreen mode Exit fullscreen mode
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 }
  );
});
Enter fullscreen mode Exit fullscreen mode

How it works:

  1. Request the user's current position (triggers permission prompt)
  2. Place the marker at those coordinates
  3. Reverse geocode to get the address
  4. Fill the autocomplete field with the formatted address

The enableHighAccuracy: true option requests GPS-level precision on mobile devices.

Marker placed at user's current GPS location with address auto-filled in the input

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;
}
Enter fullscreen mode Exit fullscreen mode

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 }
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

This helps you understand how much the user adjusted the location - useful for analytics or quality checks.

Confirmation message showing saved coordinates and address

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

  1. Search - Type in the autocomplete field and select an address
  2. Confirm - Pin appears on the map at the selected location
  3. Adjust - Drag the pin to fine-tune the exact spot
  4. 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:

  1. Address autocomplete - fast search with real-time suggestions (primary input)
  2. 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:


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)