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:truewhen another page exists.X-Next-Cursor: pass this value ascursoron the next request. Omitcursorfor 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.
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_tilesdefaults to 2, and must be a power of 2.1keeps the box you passed. A larger value splits the box and runs each tile in order.max_featuresdefaults to 55,000.Noneremoves that client cap. API rate limits still apply.timeoutdefaults to 60 seconds for the whole drain.Nonewaits with no wall-clock cap.- Do not pass
limitorcursor.query_allowns both. - GeoJSON only. CSV, TSV, FlatGeobuf, and GeoParquet stay on
query()plus a cursor loop. meta.has_moremeansquery_allstopped atmax_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.
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);bboxTilesdefaults to 2 and must be a power of 2.1keeps one box.maxFeaturesdefaults to 55,000.nullremoves that client cap.maxPagesdefaults to 15 pages per tile. Raise it for a dense box at page size 10,000.meta.has_moreormeta.relay_partialmeans the drain stopped early: the feature cap,maxPages, or a later page was rejected or rate limited.- GeoJSON only. Other
Accepttypes usequery()andmeta.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.
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.