Logo
Published on

Building a Powerful Geocoding Solution with OpenCage and Thunderforest Maps

Authors
  • avatar
    Name
    Arnaud Ferrand
    Twitter

Introduction

cover

Interactive maps have become an essential component of modern web applications, from ride-sharing platforms to real estate websites. One of the most crucial features users expect is the ability to search for locations by typing addresses or place names – a process known as geocoding. While there are many geocoding services available, finding the right combination of accuracy, performance, and visual appeal can be challenging.

In this tutorial, we'll explore how to implement a robust geocoding solution using two powerful services: OpenCage's geocoding API and Thunderforest's beautiful map tiles. OpenCage provides reliable, worldwide geocoding with excellent coverage and detailed location data. At the same time, Thunderforest offers stunning, professionally designed map styles that can elevate your application's visual appeal far beyond standard map tiles.

Tutorial

First, sign up for the services, respectively:

Import the dependencies via the CDN unpkg:

<!-- Load MapLibre GL -->
<link
  rel="stylesheet"
  href="https://unpkg.com/maplibre-gl@5.6.1/dist/maplibre-gl.css"
/>
<script src="https://unpkg.com/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>

<!-- Load MapLibre Geocoder plugin -->
<link
  rel="stylesheet"
  href="https://unpkg.com/@maplibre/maplibre-gl-geocoder@1.5.0/dist/maplibre-gl-geocoder.css"
/>
<script src="https://unpkg.com/@maplibre/maplibre-gl-geocoder@1.5.0/dist/maplibre-gl-geocoder.min.js"></script>

<!-- Load Opencage API Client -->
<script src="https://unpkg.com/opencage-api-client@2.0.0/dist/opencage-api.min.js"></script>

Anchor the map to an HTML div, and don't forget to give the map a height:

<div id="map"></div>
#map {
  height: 100%;
}

Create the map using a Thunderforest Vector Tiles style:

const map = new maplibregl.Map({
  container: 'map',
  style: `https://api.thunderforest.com/styles/atlas/style.json?apikey=THUNDERFOREST_API_KEY`, // Replace with your Thunderforest API key
  center: [-0.1275, 51.507222],
  zoom: 3,
  canvasContextAttributes: { antialias: true },
});

Please remember to replace the Thunderforest API key.

Then add the geocoder plugin:

map.addControl(
  new MaplibreGeocoder(geocoderApi, {
    maplibregl,
  })
);

All we need now is to define the geocoder API where we use the OpenCage Client API

const geocoderApi = {
  forwardGeocode: async (config) => {
    const features = [];
    try {
      const response = await opencage.geocode({
        key: 'OPENCAGE_API_KEY', // Replace with your OpenCage API key
        q: config.query,
        limit: 5, // Limit results to 5
        no_annotations: 1, // Exclude annotations for simplicity
      });
      if (response.results && response.results.length > 0) {
        for (const result of response.results) {
          const center = [result.geometry.lng, result.geometry.lat];
          const point = {
            type: 'Feature',
            geometry: {
              type: 'Point',
              coordinates: center,
            },
            place_name: result.formatted,
            properties: result.components,
            text: result.formatted,
            place_type: ['place'],
            center,
          };
          features.push(point);
        }
      }
    } catch (e) {
      console.error(`Failed to forwardGeocode with error: ${e}`);
    }

    return {
      features,
    };
  },
};

The API request uses OpenCage's geocode method with the query and some configuration options (limiting to 5 results and excluding annotations for shorter responses). Please remember to replace the OpenCage API key. The results are then transformed from OpenCage's JSON format into GeoJSON Feature objects that MapLibre GL JS expects, with each location converted into a standardised structure containing coordinates, formatted address, and place properties.

Full Code

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Geocode with OpenCage</title>
    <meta
      property="og:description"
      content="Geocode with Opencage and the maplibre-gl-geocoder plugin."
    />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1.0"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/maplibre-gl@5.6.1/dist/maplibre-gl.css"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/@maplibre/maplibre-gl-geocoder@1.5.0/dist/maplibre-gl-geocoder.css"
    />
    <script src="https://unpkg.com/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
    <script src="https://unpkg.com/@maplibre/maplibre-gl-geocoder@1.5.0/dist/maplibre-gl-geocoder.min.js"></script>
    <script src="https://unpkg.com/opencage-api-client@2.0.0/dist/opencage-api.min.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
      }

      html,
      body,
      #map {
        height: 100%;
      }
    </style>
  </head>

  <body>
    <div id="map"></div>
    <script>
      const map = new maplibregl.Map({
        container: 'map',
        style: `https://api.thunderforest.com/styles/atlas/style.json?apikey=THUNDERFOREST_API_KEY`, // Replace with your Thunderforest API key
        center: [-0.1275, 51.507222],
        zoom: 3,
        canvasContextAttributes: { antialias: true },
      });

      const geocoderApi = {
        forwardGeocode: async (config) => {
          const features = [];
          try {
            const response = await opencage.geocode({
              key: 'OPENCAGE_API_KEY', // Replace with your OpenCage API key
              q: config.query,
              limit: 5, // Limit results to 5
              no_annotations: 1, // Exclude annotations for simplicity
            });
            if (response.results && response.results.length > 0) {
              for (const result of response.results) {
                const center = [result.geometry.lng, result.geometry.lat];
                const point = {
                  type: 'Feature',
                  geometry: {
                    type: 'Point',
                    coordinates: center,
                  },
                  place_name: result.formatted,
                  properties: result.components,
                  text: result.formatted,
                  place_type: ['place'],
                  center,
                };
                features.push(point);
              }
            }
          } catch (e) {
            console.error(`Failed to forwardGeocode with error: ${e}`);
          }

          return {
            features,
          };
        },
      };
      map.addControl(
        new MaplibreGeocoder(geocoderApi, {
          maplibregl,
        })
      );
    </script>
  </body>
</html>

Conclusion

The OpenCage API proved to be reliable and comprehensive, handling various types of location queries with impressive accuracy. At the same time, Thunderforest's map tiles added that professional polish that sets your application apart from the crowd. MapLibre GL JS served as the perfect foundation, providing smooth performance and extensive customisation options.

Key takeaways from this implementation:

  • OpenCage offers excellent geocoding accuracy with generous free tier limits
  • Thunderforest's styled maps can significantly enhance the user experience
  • MapLibre GL JS provides a robust, open-source foundation for modern mapping applications

What's Next?

While our JavaScript implementation works, the modern web development landscape is increasingly embracing TypeScript for its type safety and developer experience benefits, along with build tools like Vite for lightning-fast development workflows.

In our next blog post, we'll take this same concept and rebuild it using contemporary development practices. Stay tuned for a deep dive into web mapping development!

Wording with modern help: Gramarly, and Claude.