Skip to main content
How to Build a City Events Map: Venues, Categories, and Live Discovery
Tutorials

How to Build a City Events Map: Venues, Categories, and Live Discovery

A practical guide to building a city events map: geocode venues, plot and cluster markers, filter by category and neighbourhood, and make listings discoverable.

Brent van der Heiden6 min read
#events map#interactive map#geocoding#poi search#javascript map#local discovery

Most local discovery products live or die on one screen: the map. A user opens your app to answer a simple question, what is on near me right now, and the map is how they answer it. Getting that map right, with the correct pins, fast filtering, and listings that search engines can actually read, is the core engineering work behind any events guide.

This guide walks through building a city events map from the ground up: modelling event data, geocoding venues to coordinates, plotting and clustering markers, filtering by category and neighbourhood, and making the listings discoverable. It assumes you already know how to render a base map. If you do not, start with our tutorial on how to add interactive maps to your website and come back here for the events-specific parts.

Model the Event Around Its Venue

An event is a time-bound thing that happens at a place. The place is what goes on the map, so the venue is the anchor of your data model. A minimal event record looks like this:

const event = {
  id: 'evt_8471',
  title: 'Late Night Jazz at The Vortex',
  category: 'live-music',
  venue: 'The Vortex Jazz Club, 11 Gillett Square, London N16 8AZ',
  coords: null,        // filled in by geocoding, once
  neighbourhood: null, // filled in by reverse geocoding, once
  startsAt: '2026-07-09T20:30:00Z',
  url: 'https://example.com/tickets/8471',
};

The two null fields matter. You do not want to geocode a venue every time the map loads. Geocode once, when the event is created or imported, then store the coordinates and treat them as permanent.

Step 1: Geocode Each Venue to Coordinates

Geocoding turns a human address into a latitude and longitude you can plot. Send the venue string to the Geocoding API and read the coordinates from the top result:

async function geocodeVenue(address) {
  const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
  url.searchParams.set('text', address);
  url.searchParams.set('size', '1');

  const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
  const data = await res.json();
  const top = data.features?.[0];
  if (!top) return null;

  const [lon, lat] = top.geometry.coordinates;
  return { lat, lon, label: top.properties.label };
}

Run this at write time, when an event enters your system, and persist the returned coordinates on the record. For a full explanation of why the exact pin location matters, and how rooftop coordinates differ from the door people actually walk to, see building-entrance geocoding.

Step 2: Plot Events as Markers

With coordinates on every event, plotting is a loop. Give each category its own colour so the map is readable at a glance:

const CATEGORY_COLORS = {
  'live-music': '#E75480',
  'theatre':    '#006BA6',
  'comedy':     '#EE7C0E',
  'food':       '#16A34A',
  'nightlife':  '#7C3AED',
};

function plotEvents(map, events) {
  events.forEach((evt) => {
    if (!evt.coords) return;
    const popup = new mapmetricsgl.Popup().setHTML(
      `<strong>${evt.title}</strong><br>${new Date(evt.startsAt).toLocaleString()}` +
      `<br><a href="${evt.url}">Tickets</a>`,
    );
    new mapmetricsgl.Marker({ color: CATEGORY_COLORS[evt.category] || '#0c3456' })
      .setLngLat([evt.coords.lon, evt.coords.lat])
      .setPopup(popup)
      .addTo(map);
  });
}

This is the pattern behind real products. A live example is OnlyHere, a daily guide to things to do in London that geocodes every venue and plots the day's events onto an interactive MapAtlas map, so a user can browse what is on by venue and neighbourhood rather than scrolling a flat list.

Step 3: Cluster When the City Fills Up

Individual markers are fine for a handful of events. A city-wide feed is not a handful. Once you pass roughly 100 to 200 pins, move the events into a GeoJSON source and let the map cluster them:

map.addSource('events', {
  type: 'geojson',
  cluster: true,
  clusterRadius: 50,
  data: {
    type: 'FeatureCollection',
    features: events.filter((e) => e.coords).map((e) => ({
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [e.coords.lon, e.coords.lat] },
      properties: { title: e.title, category: e.category },
    })),
  },
});

Now a busy night in the centre of town shows a single bubble with a count instead of a hundred overlapping pins, and the cluster splits apart as the user zooms in.

Step 4: Filter by Category and Neighbourhood

Filtering is where an events map becomes an events product. Keep the full list in memory and filter the array, then hand the filtered set to the map source rather than rebuilding markers by hand:

function applyFilters(map, allEvents, { category, neighbourhood }) {
  const filtered = allEvents.filter((e) =>
    (!category || e.category === category) &&
    (!neighbourhood || e.neighbourhood === neighbourhood),
  );
  map.getSource('events').setData({
    type: 'FeatureCollection',
    features: filtered.filter((e) => e.coords).map((e) => ({
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [e.coords.lon, e.coords.lat] },
      properties: { title: e.title },
    })),
  });
  return filtered;
}

To populate the neighbourhood field, reverse geocode each venue's coordinates once and store the admin area name. That single value powers a neighbourhood dropdown, a "what's on in this area" view, and a cleaner listing page, all without another lookup at run time.

Step 5: Make the Listings Discoverable

A map answers the question for the person already in your app. It does nothing for the far larger audience asking a search engine or an AI assistant, "what's on in Shoreditch tonight". Those systems read structured data, not pixels.

Mark up every event with schema.org Event fields, and reuse the coordinates you already have:

{
  "@context": "https://schema.org",
  "@type": "Event",
  "name": "Late Night Jazz at The Vortex",
  "startDate": "2026-07-09T20:30:00Z",
  "location": {
    "@type": "Place",
    "name": "The Vortex Jazz Club",
    "geo": { "@type": "GeoCoordinates", "latitude": 51.5462, "longitude": -0.0753 }
  }
}

The coordinates that draw the pin also feed the structured data, so display and discovery come from one source of truth. This is the difference between a map that looks good and a product that gets found. For more on why machine-readable location data drives AI search visibility, see our guide to location-specific FAQs for AI search.

Bringing It Together

A city events map is five moving parts: a venue-anchored data model, geocoding at write time, coloured markers, clustering at scale, and array-based filtering, all sitting on top of structured data that keeps the listings discoverable. Build those parts on one geospatial platform and the coordinates flow cleanly from the geocoder to the map to the schema, with no duplicated lookups and no drifting data.

MapAtlas gives you the geocoding, reverse geocoding, place search, and map rendering to build all of it on EU-hosted, GDPR-compliant infrastructure. Explore the Geocoding API and Search API to start plotting your own city.

Frequently Asked Questions

What data do I need to build a city events map?

At minimum, each event needs a venue address, a category, and a start time. The address is what turns an event into a map pin: you geocode it once to get latitude and longitude, store those coordinates, and reuse them every time the event is shown. Everything else, such as ticket links, images, and descriptions, is metadata you attach to the marker's popup.

How do I turn a venue address into map coordinates?

Call a geocoding API with the venue's address string and read back the latitude and longitude from the response. With the MapAtlas Geocoding API you send the address as a text query to https://api.mapatlas.eu/geocoding/v1/search and use the coordinates from the top result. Geocode each venue once when the event is created, not on every page load, so the map stays fast and your API usage stays low.

How many event markers can a map show before it slows down?

Rendering individual markers stays smooth up to roughly 100 to 200 pins on a zoomed-out view. Beyond that, switch to GeoJSON source clustering, which groups nearby events into a single count bubble at low zoom and splits them apart as the user zooms in. A city-wide events feed with hundreds of listings should use clustering from the start.

How do I let users filter events by category or neighbourhood?

Keep the full event list in memory as an array, attach a category and a neighbourhood to each item, and filter that array in response to the user's selection. Then update the map source with the filtered set instead of destroying and rebuilding markers. Browsing by neighbourhood works the same way: filter to the events whose coordinates fall inside the selected area, or that share its admin name from reverse geocoding.

Why does an events map need structured data?

A visual map helps humans, but search engines and AI assistants read structured data. Marking up each event with schema.org Event fields, including the venue's name, coordinates, and start time, is what lets a listing surface in answers to questions like 'what's on in Shoreditch tonight'. The same coordinates you plot on the map feed the structured data, so discovery and display come from one source of truth.

Found this useful? Share it.

About the author

Brent van der Heiden

Written by

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. He writes about geospatial infrastructure, AI search visibility, and how location data powers the products people rely on every day.

View all articles
Back to Blog