How To Query All Features

GET /v2/osm_features returns at most 10,000 features in one response. When more features match, request the next page. query_all in osmfeatures-py and osmfeatures-ts requests every page for you. The same loop is a few lines of your own HTTP client.

The feature limit cap

limit is the page size. The default is 1,000. The server rejects a limit above 10,000. Every response carries pagination in headers:

  • X-Returned: features in this page.
  • X-Has-More: true when another page exists.
  • X-Next-Cursor: pass this value as cursor on the next request. Omit cursor for the first page.

Stop when X-Has-More is false. A short page is the last page. A page of 10,000 features with X-Has-More: true means more features match.

Do not paginate GeoJSON to count. Use Counts (GET /v2/osm_features/stats) for tag histograms.

Python

OSMFeaturesClient.query_all follows X-Next-Cursor until the query is exhausted or a client cap stops it. The call below fetches building polygons in a Stockholm box. limit_per_page is the HTTP page size. Use 10,000 when you want the largest page the API allows.

Python
from osmfeatures import OSMFeaturesClient

with OSMFeaturesClient(api_key="swHAvOmreIm_uew6eqbn1UbIsVQT6p8PRohmqmAJiu4") as client:
    fc = client.query_all(
        bbox="18.05,59.32,18.10,59.34",
        type="way",
        way_shape="polygon",
        tags="building",
        limit_per_page=10_000,
        bbox_tiles=1,
        max_features=None,
    )
print(len(fc.features), "buildings")
print("more remain" if fc.meta.has_more else "complete")
  • bbox_tiles defaults to 2, and must be a power of 2. 1 keeps the box you passed. A larger value splits the box and runs each tile in order.
  • max_features defaults to 55,000. None removes that client cap. API rate limits still apply.
  • timeout defaults to 60 seconds for the whole drain. None waits with no wall-clock cap.
  • Do not pass limit or cursor. query_all owns both.
  • GeoJSON only. CSV, TSV, FlatGeobuf, and GeoParquet stay on query() plus a cursor loop.
  • meta.has_more means query_all stopped at max_features. The API may still have more rows.

TypeScript

OSMFeatures.query_all is the same drain: cursor pages, optional bbox tiles, then one FeatureCollection. Parameter names are camelCase.

TypeScript
import { OSMFeatures } from 'osmfeatures';

const client = new OSMFeatures('swHAvOmreIm_uew6eqbn1UbIsVQT6p8PRohmqmAJiu4');
const result = await client.query_all({
  bbox: '18.05,59.32,18.10,59.34',
  type: 'way',
  wayShape: 'polygon',
  tags: ['building'],
  limitPerPage: 10_000,
  bboxTiles: 1,
  maxFeatures: null,
});
console.log(result.data.features.length, result.meta.has_more);
  • bboxTiles defaults to 2 and must be a power of 2. 1 keeps one box.
  • maxFeatures defaults to 55,000. null removes that client cap.
  • maxPages defaults to 15 pages per tile. Raise it for a dense box at page size 10,000.
  • meta.has_more or meta.relay_partial means the drain stopped early: the feature cap, maxPages, or a later page was rejected or rate limited.
  • GeoJSON only. Other Accept types use query() and meta.next_cursor.

Custom code

Any HTTP client can do the same thing. Request limit=10000, append features, and repeat with cursor while X-Has-More is true. Each page is its own billed request. Use this loop for CSV, TSV, FlatGeobuf, and GeoParquet. query_all returns GeoJSON.

fetch
const features = [];
let cursor = null;

do {
  const url = new URL('https://api.maplark.com/v2/osm_features');
  url.searchParams.set('bbox', '18.05,59.32,18.10,59.34');
  url.searchParams.set('type', 'way');
  url.searchParams.set('way_shape', 'polygon');
  url.searchParams.set('tags', 'building');
  url.searchParams.set('limit', '10000');
  if (cursor) url.searchParams.set('cursor', cursor);

  const res = await fetch(url, {
    headers: { Authorization: 'Bearer swHAvOmreIm_uew6eqbn1UbIsVQT6p8PRohmqmAJiu4' },
  });
  if (!res.ok) throw new Error(await res.text());

  const page = await res.json();
  features.push(...page.features);
  cursor = res.headers.get('X-Has-More') === 'true'
    ? res.headers.get('X-Next-Cursor')
    : null;
} while (cursor);

Caps that still apply

Your tier's bounding-box area and radius cap still apply to each request. bbox_tiles splits the box so each request is smaller. Features that sit on a tile edge are deduplicated by id inside query_all.

A cursor walk of one box, as in the fetch example, stays inside the box you sent. If that box is over the tier cap, split it or pass bbox_tiles above 1.

See also