Skip to main content
Being Idea Innovations
Geolocation and Google Maps Directions: A Working 2026 Guide
Back to Blog

Geolocation and Google Maps Directions: A Working 2026 Guide

15 July 20187 min readJavascript
Share:

Asking the browser where someone is and drawing a route from there to a destination is a common feature — store locators, delivery estimates, "directions to us" pages. The code that circulates for it is mostly from before 2018 and no longer runs.

This guide covers what actually works now: the current Maps loader, the permission model, and what to do when a user says no.

What changed since the old tutorials

If you have copied a snippet like this, it cannot work:

<!-- Broken for years -->
<script src="http://maps.google.com/maps/api/js?sensor=true"></script>
  • No API key. Since June 2018 the Maps JavaScript API requires a key and a billing account. Without one you get a dark "for development purposes only" watermark or nothing at all.
  • The sensor parameter was removed in 2013. It is ignored now, but its presence marks the snippet's age.
  • Plain HTTP. The API is HTTPS-only.
  • Geolocation requires a secure context. Chrome removed navigator.geolocation from insecure origins in version 50. On http:// the object is simply not there — which is also why it works on localhost (treated as secure) and then fails on a staging server.

Loading the API

Google's current approach is the bootstrap loader with importLibrary(), which fetches libraries on demand instead of pulling everything up front:

<script>
  (g => { let h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary",
    q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {});
    const d = b.maps || (b.maps = {}), r = new Set(), e = new URLSearchParams(),
    u = () => h || (h = new Promise(async (f, n) => {
      a = m.createElement("script"); e.set("libraries", [...r] + "");
      for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]);
      e.set("callback", c + ".maps." + q);
      a.src = `https://maps.${c}apis.com/maps/api/js?` + e;
      d[q] = f; a.onerror = () => h = n(Error(p + " could not load."));
      m.head.append(a);
    }));
    d[l] ? console.warn(p + " only loads once. Ignoring:", g)
         : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n));
  })({ key: "YOUR_API_KEY", v: "weekly" });
</script>

Then load only the libraries you need, when you need them:

const { Map }              = await google.maps.importLibrary('maps');
const { DirectionsService, DirectionsRenderer } = await google.maps.importLibrary('routes');
const { Geocoder }         = await google.maps.importLibrary('geocoding');

Restrict your key

Your key is visible in page source — that is unavoidable. Protect it in the Google Cloud Console instead:

  • Application restriction: HTTP referrers, listing only your domains.
  • API restriction: only Maps JavaScript API, Geocoding, Directions — whatever you actually use.
  • Set a budget alert. Maps is billed per load. An unrestricted key scraped from your source and used elsewhere gets charged to you.

Never put a key with Directions or Geocoding server-side permissions in client code — those are billed per request and are the ones worth stealing.

Getting the user's position

function getPosition(options = {}) {
  return new Promise((resolve, reject) => {
    if (!('geolocation' in navigator)) {
      reject(new Error('unsupported'));
      return;
    }

    navigator.geolocation.getCurrentPosition(resolve, reject, {
      enableHighAccuracy: true,
      timeout: 10_000,
      maximumAge: 60_000,     // accept a cached fix up to a minute old
      ...options,
    });
  });
}

The three options each matter. enableHighAccuracy asks for GPS rather than a network estimate — more precise, slower, and heavier on battery. timeout prevents hanging forever when a fix never arrives, which happens indoors. maximumAge lets the browser return a recent cached position instantly instead of powering up the GPS again.

Handle refusal properly

This is the part most implementations skip. Users decline location prompts often, and a page that just breaks is worse than one that never asked.

async function fillFromLocation(inputId) {
  const input = document.getElementById(inputId);
  const status = document.getElementById('status');

  status.textContent = 'Finding your location…';

  try {
    const position = await getPosition();
    const { latitude, longitude } = position.coords;

    const { Geocoder } = await google.maps.importLibrary('geocoding');
    const { results } = await new Geocoder().geocode({
      location: { lat: latitude, lng: longitude },
    });

    if (!results.length) throw new Error('no-address');

    input.value = results[0].formatted_address;
    status.textContent = '';
  } catch (err) {
    status.textContent = describeError(err);
    input.focus();          // fall back to typing an address
  }
}

function describeError(err) {
  switch (err.code) {
    case 1: return 'Location access was blocked. Type an address instead, or enable location in your browser settings.';
    case 2: return 'Your location could not be determined. Please type an address.';
    case 3: return 'Finding your location took too long. Please type an address.';
    default: return 'Type an address to continue.';
  }
}

Codes 1, 2 and 3 are PERMISSION_DENIED, POSITION_UNAVAILABLE and TIMEOUT. Distinguishing them lets you give advice that actually helps rather than a generic failure.

Only request location from a user gesture. Prompting on page load gets denied — and once denied, the browser will not ask again. Put it behind a "Use my location" button, exactly as the form below does.

Drawing the route

<form id="route-form">
  <div class="field">
    <label for="from">From</label>
    <input type="text" id="from" name="from" required autocomplete="street-address" />
    <button type="button" data-locate="from">Use my location</button>
  </div>

  <div class="field">
    <label for="to">To</label>
    <input type="text" id="to" name="to" required />
  </div>

  <button type="submit">Get directions</button>
  <p id="status" role="status" aria-live="polite"></p>
</form>

<div id="map" style="height: 480px"></div>
<div id="directions-panel"></div>
let map, directionsService, directionsRenderer;

async function initMap() {
  const { Map } = await google.maps.importLibrary('maps');
  const { DirectionsService, DirectionsRenderer } =
    await google.maps.importLibrary('routes');

  map = new Map(document.getElementById('map'), {
    zoom: 10,
    center: { lat: 12.9716, lng: 77.5946 },   // Bengaluru
    mapId: 'YOUR_MAP_ID',                     // required for vector maps
  });

  directionsService = new DirectionsService();

  // Create the renderer ONCE and reuse it. Creating a new one per search
  // leaves every previous route drawn on the map.
  directionsRenderer = new DirectionsRenderer({
    map,
    panel: document.getElementById('directions-panel'),
  });
}

async function calculateRoute(from, to) {
  const status = document.getElementById('status');
  status.textContent = 'Calculating route…';

  try {
    const result = await directionsService.route({
      origin: from,
      destination: to,
      travelMode: google.maps.TravelMode.DRIVING,
      unitSystem: google.maps.UnitSystem.METRIC,
      provideRouteAlternatives: true,
    });

    directionsRenderer.setDirections(result);

    const leg = result.routes[0].legs[0];
    status.textContent = `${leg.distance.text} — about ${leg.duration.text}`;
  } catch (err) {
    status.textContent = err.code === 'ZERO_RESULTS'
      ? 'No route found between those places.'
      : 'Could not calculate a route. Check the addresses and try again.';
  }
}

document.getElementById('route-form').addEventListener('submit', (e) => {
  e.preventDefault();
  calculateRoute(document.getElementById('from').value, document.getElementById('to').value);
});

document.querySelectorAll('[data-locate]').forEach((btn) => {
  btn.addEventListener('click', () => fillFromLocation(btn.dataset.locate));
});

initMap();

Two changes from the old code worth calling out. route() now returns a promise, so the callback-with-status-check pattern is gone. And google.maps.DirectionsTravelMode is the legacy alias — the current name is google.maps.TravelMode.

The single reused DirectionsRenderer is a real bug fix. The original constructed a new one inside the route callback on every search, so each new route was drawn on top of the last and none were ever cleared.

Accuracy: set expectations

The Geolocation API's accuracy varies enormously by device and context:

SourceTypical accuracyWhen used
GPS5–20 mMobile, outdoors, high accuracy on
Wi-Fi positioning20–100 mIndoors, urban
Cell tower100 m – 3 kmMobile without GPS lock
IP addressCity to countryDesktop, no other signal

Always read position.coords.accuracy — it is a radius in metres. If it comes back as 3,000, do not present the result as a precise address. Show the resolved area and let the user correct it.

Cost and alternatives

Maps is billed per map load, per geocode and per directions request. There is a monthly free allowance, but a busy store locator can exceed it. Practical reductions:

  • Cache geocoding results. An address resolves to the same coordinates forever — store them rather than re-geocoding on every view.
  • Load the map on interaction. Show a static image or placeholder and initialise the interactive map when the user clicks it.
  • Consider alternatives. Leaflet with OpenStreetMap tiles is free for display; OpenRouteService and Mapbox both offer routing with generous free tiers.

Privacy

Precise location is sensitive personal data. Request it only when the feature genuinely needs it, explain why before triggering the prompt, do not store coordinates unless you have a reason, and always provide a manual address route. A user who declines should be able to complete the same task by typing.

Summary

  • An API key with billing is mandatory; restrict it by referrer and by API.
  • Geolocation only exists on HTTPS (and localhost).
  • Request position from a user gesture, never on page load.
  • Handle all three error codes with distinct, useful messages.
  • Reuse one DirectionsRenderer rather than creating one per search.
  • Use importLibrary() and the promise-based route().
  • Check coords.accuracy before trusting a fix.
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers