DEV Community

Cover image for Handle Large Place Searches: Chunked and Rate-Limited Search in Leaflet

Handle Large Place Searches: Chunked and Rate-Limited Search in Leaflet

In the previous tutorial, we used the Geoapify Places API and Leaflet to display nearby places — like parks, museums, and cinemas — on a map.

That simple approach works well for small areas, but loading hundreds of places at once can slow the map or exceed API rate limits.

In this part, you’ll learn how to make place searches more efficient by:

  • Splitting the map area into chunks for smaller, faster requests
  • Applying rate limiting to control request flow
  • Rendering results progressively for smoother UX

Try the full example:
➡️ View on CodePen

Sure! Here’s a clean, Dev.to–ready Table of Contents for your article.
It matches your section headings exactly and uses the same anchor style Dev.to automatically generates.

🧭 Table of Contents

  1. Introduction
  2. Why large place searches can be slow
  3. A better approach
  4. Split the map bounds into smaller fragments
  5. Run fragment requests with rate limiting
  6. See the full implementation
  7. Summary

Why large place searches can be slow

When your map covers a large area — for example, an entire city — the Geoapify Places API may need to process thousands of locations at once.
If you request all of them in a single query:

  • The API call can take longer to complete.
  • The response size becomes large, increasing data transfer time.
  • The map may freeze while parsing or rendering the results.
  • Users experience visible lag or delayed updates.
  • It might even require querying multiple pages of results to retrieve all places in that area.

A better approach

To keep your app fast and user-friendly, it’s better to:

  1. Split the map area into smaller parts (chunks)
  • Each chunk is a smaller rectangle inside the visible map bounds.
  • The app sends several lightweight queries instead of one large request.
  • Results load progressively — users start seeing places immediately while others continue loading in the background.

Splitting the map area into smaller queries

Description: A map view divided into several rectangles, each labeled “API request,” showing places being loaded one fragment at a time. Arrows can indicate parallel or sequential loading.

  1. Apply rate limiting

When you send multiple requests in parallel — for example, one per map fragment — it’s important to control how often they are executed.

Without rate limiting, dozens of simultaneous API calls can overload the network or exceed the API’s allowed request rate, leading to HTTP 429 (Too Many Requests) errors.

  • The Geoapify Request Rate Limiter automatically spaces out requests over time.
  • It ensures you stay within the API’s limits, keeps the map responsive, and provides a smooth, continuous loading experience for users.

Too Many Requests — When Rate Limiting Is Missing

Description: If rate limits are not respected, some API requests return 429 (Too Many Requests) errors.
In this example, the API allows 5 requests per second, but 9 are sent simultaneously — so 4 fail. The map shows those failed fragments highlighted in red.


Now that we understand why large queries can cause slow responses or 429 errors, let’s look at how to split the visible map area into smaller fragments.

Split the map bounds into smaller fragments

To make large-area searches more efficient, we’ll divide the visible map area into several smaller rectangles, called fragments.

For example, the following helper function decides how many pieces the map should be split into depending on the zoom level (fewer at high zoom, more at low zoom):

function getAxisChunksForZoom(zoom) {
  const z = Math.floor(zoom);
  const delta = Math.max(0, 17 - z);
  if (delta === 0) return 1;        // At zoom ≥ 16, one request is enough
  return delta;   // At lower zooms, split the area into more parts
}
Enter fullscreen mode Exit fullscreen mode
  • The function takes the current map zoom level as input.
  • When you’re zoomed in close (zoom ≥ 16), the visible area is small, so only one fragment (one request) is needed.
  • As you zoom out, the visible area grows. The function increases the number of fragments exponentially — for example:

    • At zoom 15 → 2 x 2 fragments per axis
    • At zoom 14 → 3 x 3 fragments
    • At zoom 13 → 4 x 4 fragments
  • This ensures that large views (like an entire city) are split into multiple smaller bounding boxes, each of which can be queried quickly and displayed independently.

Next, we’ll use this helper to actually divide the map bounds into fragments — small rectangular areas that together cover the visible region.

The following function, getFragments(bounds, zoom), takes the current map bounds and zoom level, then returns an array of rectangles. Each rectangle defines a mini viewbox that we’ll later use to query the Geoapify Places API:

function getFragments(bounds, zoom) {
  if (!bounds) return [];

  const chunks = getAxisChunksForZoom(zoom);
  const sw = bounds.getSouthWest();
  const ne = bounds.getNorthEast();

  // If only one chunk, return the full map bounds
  if (chunks <= 1) {
    return [{ minLng: sw.lng, minLat: sw.lat, maxLng: ne.lng, maxLat: ne.lat }];
  }

  const latStep = (ne.lat - sw.lat) / chunks;
  const lngStep = (ne.lng - sw.lng) / chunks;
  const rects = [];

  // Loop through rows and columns to build smaller rectangles
  for (let row = 0; row < chunks; row += 1) {
    const minLat = sw.lat + row * latStep;
    const maxLat = row === chunks - 1 ? ne.lat : minLat + latStep;

    for (let col = 0; col < chunks; col += 1) {
      const minLng = sw.lng + col * lngStep;
      const maxLng = col === chunks - 1 ? ne.lng : minLng + lngStep;
      rects.push({ minLng, minLat, maxLng, maxLat });
    }
  }

  return rects;
}
Enter fullscreen mode Exit fullscreen mode

What this does

  • Calculates how many pieces (chunks) the area should be split into based on zoom level.
  • Divides the visible map into a grid of rectangles, using latitude and longitude steps.
  • Returns an array of bounding boxes — each one ready to be used in a request like:
  filter=rect:minLon,minLat,maxLon,maxLat
Enter fullscreen mode Exit fullscreen mode

As a result, instead of sending one large, heavy API query, we can make multiple smaller requests — one per fragment — and load results progressively and smoothly.

Run fragment requests with rate limiting

Now that we can split the view into fragments and build one request per fragment, let’s execute them safely.
We’ll use [@geoapify/request-rate-limiter](https://www.npmjs.com/package/@geoapify/request-rate-limiter) to pace requests so the UI stays smooth and we avoid bursts.

// Build one async function per fragment (from previous step)
const requestFns = fragments.map(({ minLng, minLat, maxLng, maxLat }) => {
  const rect = `${minLng},${minLat},${maxLng},${maxLat}`;
  const url = `https://api.geoapify.com/v2/places?categories=${encodeURIComponent(categories)}&filter=rect:${rect}&limit=200&apiKey=${yourAPIKey}`;

  return async () => {
    const resp = await fetch(url);
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    const data = await resp.json();
    renderResults(data); // progressive rendering: draw as each fragment returns
    return data;
  };
});

// Run with rate limiting: 5 requests per 1000 ms window
const results = await RequestRateLimiter.rateLimitedRequests(
  requestFns,
  5,          // max requests per window
  1000,       // window duration (ms)
  {
    onProgress: ({ completedRequests, totalRequests }) => {
      // optional: update a small status label
      // e.g., setStatus(`Loaded ${completedRequests}/${totalRequests} fragments…`);
    }
  }
);
Enter fullscreen mode Exit fullscreen mode
  • Paces outgoing requests to avoid spikes and keep the app responsive.
  • Renders progressively: users see places appear fragment-by-fragment, not all at once.
  • Keeps your code simple: pass an array of tasks and let the limiter schedule them.

See the full implementation

You can explore the complete working version in the CodePen example:
➡️ View on CodePen

The code sample demonstrates everything described in this tutorial — including:

  • Rate-limited API requests to keep the app smooth and avoid 429 errors
  • Canceling outdated searches when the user moves or zooms the map again
  • Progressive visualization of results as each fragment loads

Together, these techniques make large place searches fast, stable, and user-friendly — even when displaying thousands of points across a wide area.

Summary

In this tutorial, we improved the performance of large-area searches in Leaflet using the Geoapify Places API.

You learned how to:

  • Split the visible map area into smaller fragments for faster, lighter API calls
  • Apply rate limiting with the Geoapify Request Rate Limiter to avoid 429 errors
  • Load and render places progressively for a smoother, more responsive user experience

You can see the complete implementation — including canceling outdated searches and visualizing data — in the live CodePen example:
➡️ View the complete example

Top comments (0)