npm package for the MapLark OSM Features API. Fetch OpenStreetMap buildings, roads, parks, and POIs as GeoJSON, FlatGeobuf, GeoParquet, or CSV from Node.js or the browser without standing up Overpass or converting extracts by hand. Simply search by tag and bounding box or location + radius. The API keeps OSM semantics intact, like node, way, relation, and returns GeoJSON Features you can feed straight into Leaflet, MapLibre, OpenLayers, or any geospatial toolchain. Use this lib to build geospatial apps on OSM easily without hitting rate limits or setting up complex and expensive infrastructure yourself.
The backend is dedicated PostGIS, not the public Overpass endpoint, with API keys and rate limits so map tiles and POI queries stay fast under load. This SDK also covers local-search and mobility: amenity lookup, OSM opening hours, and walk or bicycle routing.
OSM types map to GeoJSON the way GIS tools expect:
node→ Pointway→ LineString or Polygonrelation→ MultiPolygon or a bundle of geometries
Filters use ordinary OSM tags (amenity=cafe, building=yes). If you already write Overpass or edit OSM, the same keys work here. Drop a FeatureCollection into Leaflet, MapLibre, OpenLayers, or Turf.
Use way_shape when you need lines vs areas:
way_shape=line— unclosed ways (streets, footpaths, rivers) and line-like relations (routes, some boundaries)way_shape=polygon— closed ways (building footprints, parks) and multipolygon relationsway_shape=all— both (the default if you leave it off)
Buildings in a box: type=way & tags=building — the same idea as Overpass way[building].
npm install osmfeaturesimport { OSMFeatures } from 'osmfeatures';
const client = new OSMFeatures('sk-...');
const page = await client.query({
bbox: '18.06,59.32,18.09,59.34',
tags: ['building'],
});
// Get GeoJSON FeatureCollection
console.log(page.data.features.length);
// Binary / table encodings via Accept param
const fgb = await client.query({
bbox: '18.06,59.32,18.09,59.34',
tags: ['building'],
accept: 'application/flatgeobuf',
});
console.log(fgb.data);Talks to https://api.maplark.com by default.
query() calls GET /v3/osm_features and returns the whole tile. Returns { data, meta } where data is a GeoJSON FeatureCollection (default) or an ArrayBuffer for binary encodings.
Omit limit for the API default. Paid keys may raise limit up to their max_limit (enterprise 1000000). A larger match set is truncated (X-Has-More: true). Pass autoSplit: true to read max_limit from GET /v1/account/tier, count first, and jump to a power-of-2 tile grid when the match set exceeds that cap (one extra jump if a leftover leaf is still dense). A lower limit truncates and does not split. A bbox-area 400 jumps to the tile size in the error, up to 32 tiles. That adds latency. Pass bboxTiles: 2 (or 4, 8, ...) to split the bbox up front. splitUntilFit is a deprecated alias for autoSplit. query does not take cursor.
The geographical area for the request in terms of GPS coordinates or specific OSM ids.
| Param | Type | Description |
|---|---|---|
bbox |
string |
Bounding box as min_lon,min_lat,max_lon,max_lat. |
location |
string |
Point for a radius search as lat,lng. Requires radius. |
radius |
number |
Search radius in metres. Requires location. |
within |
string |
Polygon spatial anchor as way/<id> or relation/<id>. Mutually exclusive with bbox / location. |
osmIds |
string |
Comma-separated OSM IDs to fetch by id. |
The feature tags to filter on.
| Param | Type | Description |
|---|---|---|
tags |
string | string[] |
A list ANDs its items, sent as one tags value joined with ,. | inside one item is OR. a|(b,c) is a OR (b AND c). ! prefixes one clause (['amenity=cafe', 'wheelchair=yes|!cuisine']). Also key!=value, key>n, key~regex. Quote a value that contains | or & (name="Ben & Jerry's"). Unquoted & is not an operator. Regex cannot use an index, so AND it with an indexed tag. |
Geometric filters such specific OSM element type, min length, or including centroid.
| Param | Type | Default | Description |
|---|---|---|---|
type |
string |
all | OSM element types, e.g. node, way, relation, or comma-separated (way,relation). |
wayShape |
`line | polygon | all` |
centroid |
boolean |
false |
When true, include a centroid on non-point features. |
clipGeometry |
boolean |
false |
When true, clip returned geometry to the requested bbox. Omit or false for full geometry. |
minLengthM |
number |
Minimum length in metres (lines). | |
maxLengthM |
number |
Maximum length in metres (lines). | |
minAreaM2 |
number |
Minimum area in square metres (polygons). | |
maxAreaM2 |
number |
Maximum area in square metres (polygons). |
Output format, tiling, and budget.
| Param | Type | Default | Description |
|---|---|---|---|
accept |
string |
application/geo+json |
Response media type in header. Options - application/geo+json, text/csv, text/tab-separated-values, application/flatgeobuf, and application/vnd.apache.parquet. |
bboxTiles |
number |
1 |
Split bbox into this many tiles (must be a power of 2: 1, 2, 4, 8, …). Use when a bbox exceeds your tier area cap. |
autoSplit |
boolean |
false |
Count first, then fetch. Jumps to a power-of-2 grid when matches exceed the key's max_limit. Caps at 32 tiles. splitUntilFit is a deprecated alias. |
timeout |
`number | null` | 60 |
zoom |
number |
Map zoom hint (used by presets / server-side simplification policies). |
| Field | Description |
|---|---|
returned |
Features in the merged result. |
has_more |
true when the tile was truncated (X-Has-More). |
units_charged |
Usage when present. After bboxTiles / autoSplit, the sum of the feature tiles. |
Use this to query a larger bbox than what is allowed by splitting the bbox up into multiple tiles and requests.
const allRestaurants = await client.query({
bbox: '18.063,59.322,18.082,59.332',
tags: ['amenity=restaurant'],
autoSplit: true,
timeout: 60,
});query_all() is the same as query().
Also exports layer presets (resolveLayerFromQuery, OSM_FEATURES_LAYER_PRESETS, ...),
GeoJSON payload helpers (featureCentroid, geometryBounds, parseFeatureId, ...),
and local helpers (nearest_within, pairs_within, around_to_bbox, point_in_geometry, isOpenNow).
Count features via GET /v2/osm_features/count. Returns { groups, total, truncated }. Spatial windows are larger than query (country-scale on every tier) and billed count-only. limit is max histogram buckets (API default 100), not a scan cap. Omit groupBy for a scalar total (groups is empty). Same tag filters as query; no osmIds, zoom, centroid, or clipGeometry. Map Express/query strings with resolveCountRequest.
const total = await client.count({
bbox: '18.05,59.32,18.10,59.34',
type: 'node',
tags: ['amenity'],
});
console.log(total.total);
const histogram = await client.count({
groupBy: 'amenity',
bbox: '18.05,59.32,18.10,59.34',
type: 'node',
tags: ['amenity'],
});
console.log(histogram.total, histogram.groups);City boundary:
const mix = await client.count({
groupBy: 'amenity',
within: 'relation/398021',
tags: ['amenity'],
});Where features are dense, via GET /v2/osm_features/density. Returns a GeoJSON FeatureCollection of occupied grid points (properties.count) plus total and truncated. Not OSM features. Omit cellDeg for 0.1. type omitted is nodes. tags is required. limit is max occupied cells. Map query strings with resolveDensityRequest.
A Europe box (-26,33,52,72) fits the density area cap and needs disableBudgetWarning: true when the per-request unit cap 400s. Count of that box still 400s on area. A unit-cap 400 on count can pass the same flag.
const bins = await client.density({
bbox: '-26,33,52,72',
tags: ['place'],
cellDeg: 0.1,
disableBudgetWarning: true,
});
console.log(bins.total, bins.features.length, bins.truncated);This month's unit-budget usage via GET /v1/usage.
const usage = await client.usage();
console.log(usage.tier, usage.usage_this_month, usage.remaining_this_month);query() is the raw OSM layer: footprints, highways, park polygons, any tag and geometry class. Places and routes sit on the same planet extract but answer product questions: amenities in a box, ranked POIs from a pin, opening hours, walk/bike isochrones, and multi-stop paths. You supply tags, extent, time, and WALK or BICYCLE. The API returns coordinates, openNow, distances, and network geometry.
Unset fields are omitted so server defaults apply (places_search limit 100, places_nearby radius 1000 m and limit 100, loop true). HTTP docs: maplark.com/developer.
places_search() looks up POIs inside a bounding box or around { lat, lon } + radius (pick one). A tags list is ANDed and sent as one comma-joined value; | inside one item is OR (['amenity=cafe', 'wheelchair=yes|!cuisine']). Leave limit off for the API default (100, max 10_000).
bboxTiles and autoSplit only apply to bbox search (same walk as query). autoSplit counts with way_shape=polygon, then fetches. No hours: split on total vs the key's max_limit. With asOf / openNow, the count ANDs opening_hours. An asOf page limit truncates; density-split vs the 10k parse cap only when limit is raised above 10k. openNow density-splits vs that 10k scan cap even with a small fill limit. After tiling, metadata.units is the sum of the tiles. location+radius cannot split. To tile a radius window, convert it first with around_to_bbox (a square that contains the circle; corner hits can fall outside the original radius):
import { around_to_bbox } from 'osmfeatures';
const bbox = around_to_bbox(18.075, 59.316, 800);
const cafes = await client.places_search({ bbox, tags: ['amenity=cafe'], autoSplit: true });const origin = { lat: 59.316, lon: 18.075 };
const cafes = await client.places_search({
location: origin,
radius: 800,
tags: ['amenity=cafe'],
openNow: true,
asOf: '2026-08-10T18:00:00+02:00',
});places_nearby() is “what is closest to this coordinate?”. You must pass tags. Hits are ordered by straight-line spheroid distance. Defaults if omitted: 1000 m radius, 100 results.
const nearby = await client.places_nearby({
location: origin,
tags: ['amenity=cafe'],
limit: 5,
openNow: true,
asOf: '2026-08-10T18:00:00+02:00',
});places_details() reloads a single OSM place by the id search or nearby gave you (node/123), or as { osmType, osmId }.
const first = (cafes.features as { id: string }[])[0];
const details = await client.places_details({ osmType: first.id });
// same as: client.places_details({ osmType: 'node', osmId: 123 })Hours are evaluated at request time in that place’s timezone.
When OSM opening_hours can be parsed, the feature gets properties.openNow as true or false. Missing or junk hours omit the field. isOpenNow(feature) keeps known-open places and unwraps the details { feature } envelope.
Timezone is inferred from coordinates (IANA). There is no timezone request field.
openNow: truedrops closed and unknown-hours POIs (Google Places–styleopenNow).asOfis the evaluation instant (default: now). An offset (Z,+02:00) is an absolute instant. A naive2026-08-10T20:00:00is local clock at the search point or bbox centre.asOforopenNowalso require anopening_hourstag, so untagged amenities do not pad the page.- Places that are closed but tagged still appear unless
openNowis set.
places_nearby ranks against one origin. “Restaurants within 150 m of a station” is two searches plus an in-process join. nearest_within does not hit the API.
import { nearest_within } from 'osmfeatures';
const bbox = '18.05,59.33,18.10,59.36';
const restaurants = await client.places_search({ bbox, tags: ['amenity=restaurant'] });
const stations = await client.places_search({ bbox, tags: ['railway=station'] });
const pairs = nearest_within(restaurants, stations, 150, { limit: 20 });
for (const pair of pairs) {
console.log(pair.distance_m, pair.feature, 'near', pair.nearest);
}Each pair is { feature, distance_m, nearest }. The point is geometry when it is a Point, otherwise properties.centroid (same as featureCentroid). Neither present throws. Empty secondary → []. Distances are spherical haversine (mean Earth radius 6371000 m). limit keeps the closest pairs (default 20); { limit: null } returns every primary with a match. More than 500000 comparisons throws — lower places_search / places_nearby limit, do not dump a large query() result. { data, meta } from query() is accepted.
pairs_within returns every pair in a distance band, not just the nearest neighbour. Same comparison cap. Pass the same collection twice for a self-join (each unordered pair once):
import { pairs_within } from 'osmfeatures';
const allPairs = pairs_within(restaurants, stations, 150, { limit: 20 });Paths follow OSM walk and bicycle ways. Default travelMode is WALK; pass 'BICYCLE' for bikes. Driving is not offered yet.
Coordinates take lon or lng. For routes_isochrone, set exactly one of maxDistanceM or durationS. searchBufferM widens the highway fetch if the default corridor cannot form a path.
const origin = { lon: 18.075, lat: 59.316 };
const cafe = { lon: 18.08, lat: 59.318 };
const iso = await client.routes_isochrone({
origin,
durationS: 600,
});
const path = await client.routes_path({
stops: [origin, cafe],
});
const tour = await client.routes_optimized_path({
start: origin,
stops: [cafe],
});
const office = { lon: 18.08, lat: 59.318 };
point_in_geometry(office.lon, office.lat, iso);In-process (no HTTP): nearest_within / pairs_within for proximity joins, around_to_bbox to tile a radius window, point_in_geometry(lon, lat, geom) for isochrone containment. The latter accepts a Polygon/MultiPolygon, a Feature, a GeometryCollection, or { geometry } from the isochrone response.
Maplark has an MCP server to integrate OpenStreetMap data into AI and LLMs such as Claude, Cursor, and Copilot. However, it is implemented in another Python sister repo. See maplark.com/products/mcp-server for more details.