Changelog#

v0.25.0 (Oct 25, 2026)#

This release makes crops and merges faster and leaner, and changes the licence: pyrosm is licensed under the BSD 3-Clause License instead of MIT. get_buildings(keep_nodes=True) also returns the buildings mapped as single nodes. With workers, crops read only the blocks each pass needs, and re-packed crops cut, decode and encode their blocks on the threads: on Finland (737 MB), a crop of the Helsinki region takes 3.8 s instead of 5.0 s, and a re-packed one 6.2 s instead of 11.6 s. Merges write on the threads, also without a region, select their elements as the threads do when they have one worker, and hold far less memory for a large region: merging two Finland extracts with 8 workers takes about 17 s instead of 25 s cut to the Helsinki region and 55 s instead of 75 s whole, and cut to all of Finland it peaks at 7 GB instead of 19 GB. Crops and merges refuse a block whose string table comes in two parts instead of writing it wrong, and a new docs page, Upgrading, lists release by release the changes that need action.

  • NEW: get_buildings(keep_nodes=True) also returns the buildings mapped as single nodes, as Points. Nodes tagged building=entrance or building=window, parts of a building, and building=no are left out, so the result holds only features that represent buildings; get_data_by_custom_criteria({"building": True}) returns every node tagged building. By default get_buildings() returns the building ways and relations, as before (#706)

  • CHANGED: pyrosm is licensed under the BSD 3-Clause License instead of MIT. The vendored khash and libdeflate keep their own licences (#718)

  • CHANGED: The package metadata states the licences as the SPDX expression BSD-3-Clause AND MIT (License-Expression, metadata version 2.4), pyrosm’s licence and that of the vendored khash and libdeflate, instead of a licence classifier, which setuptools 77 and later warn about; building pyrosm needs setuptools 77 or later (#721)

  • CHANGED: The repository’s default branch is main, renamed from master. The tests run on pushes to main, and the README, the guides to contributing and releasing, the links to files of the repository and the docs’ links to their sources name main (#723)

  • CHANGED: Crops with repack=True and workers read, decompress and decode the source blocks of their write on the worker threads; the re-packed blocks are still encoded and written in order on one thread, so the output is the same byte for byte. On Finland (737 MB), a re-packed crop of the Helsinki region with 8 workers takes 9.2-9.5 s instead of 11.4-11.6 s. With PYROSM_DECODE_BACKEND=processes the write runs in one process, as before (#725)

  • CHANGED: With workers, the node pass of a crop or merge notes which blocks hold ways and relations, and the later selection passes read only those. On Finland (737 MB), a crop of the Helsinki region with 8 workers takes 3.75-3.85 s instead of 5.0-5.1 s, and 8.4 s instead of 9.2-9.4 s with repack=True; the output is the same byte for byte (#729)

  • CHANGED: The threads of a crop with repack=True cut each source block down to its kept elements without the GIL, as the default crop does, and decode only those for the re-packed write; they decoded every element of the block. On Finland (737 MB), a re-packed crop of the Helsinki region with 8 workers takes 6.3-6.5 s instead of 8.4-9.2 s, with the same peak memory; the output is the same byte for byte (#731)

  • CHANGED: The threads of a crop with repack=True also encode and compress the re-packed blocks, which the calling thread writes in order. On Finland (737 MB), a re-packed crop of the Helsinki region with 8 workers takes 6.16 s instead of 6.55 s (medians of 8 runs); the output is the same byte for byte (#733)

  • CHANGED: A merge with a bounding_box or polygon and workers writes with them too: the threads crop and decode the inputs’ blocks and encode the merged ones, written in order; every element is still checked to be sorted by type then id. On two overlapping Finland extracts (737 and 769 MB) cut to the Helsinki region, a merge with 8 workers takes 16.7-16.9 s instead of 24.2-26.4 s; the output is the same byte for byte (#735)

  • CHANGED: Merges use their workers more and hold less memory. A merge without bounding_box or polygon runs on the worker threads, and the threads of a merge share their read-ahead among the inputs: on two overlapping Finland extracts (737 and 769 MB), a merge of the whole files with 8 workers takes 53-59 s instead of 73-76 s. The selection of a merge with a region holds each set of ids only while it needs it: cut to a box around all of Finland, the same merge peaks at 7.3 GB instead of 18.9 GB with 8 workers and at 7.0 GB instead of 17.3 GB with one. With one worker, the selection runs the passes of the threads on the calling thread and reads only the blocks each pass needs: cut to the Helsinki region, the merge takes 42 s instead of 67 s. The output is the same byte for byte (#737, #739, #741)

  • FIXED: Crops and merges refuse a block whose string table comes in two parts, which protobuf reads as one, with an InvalidOSMFileError that names the file. They read only the last part: a crop wrote the block’s elements with tags and roles beyond the written string table, and a re-packed crop failed with IndexError (#727)

  • DOCS: A new page, Upgrading, lists release by release the changes since 0.6.2 that alter what reads, downloads and graph exports return or that need a change in code, with what to do about each (#713)

  • DOCS: The links of the quickstart and of the guide to reading OSM data open their sections, and the OSM.to_pbf API page gives its paragraph about the crop’s progress as text instead of four parameters that do not exist (#716)

  • DOCS: The citations on the How to cite page, on the start page of the docs, in the README and in CITATION.cff name the current version, 0.25.0, instead of 0.13.1, and the release steps include updating them (#746)

v0.24.0 (Oct 23, 2026)#

This release finishes behaviour that worked on the common path but not everywhere. OSM.write_pbf writes an edit made in any row of an element and writes its file sorted, and written files and crops keep the members of complete relations and replace their output only when complete. Every function that takes a bounding box takes the same forms, area downloads reuse their crops, get_data_by_bbox and get_data_by_geocoding choose among Geofabrik, BBBike and Movisda extracts, and both engines give the same columns and dtypes. Reads, crops, merges, writes and graph exports show one progress from 0 to 100 % through all their steps; history reads take a tenth of the time and OSM.write_pbf about half. Several changes alter results: every read has a bool visible column, readers keep the keys of their filter as columns, a plain-dict custom_filter of get_network keeps its matches, get_boundaries(name=...) finds plain text, NetworkX graphs carry oneway and reversed as booleans, default graphs are fully simplified, a progress function is called as progress(done, 100), and closed ways are areas or lines by the OSM wiki’s polygon features, as in OSMnx, so fenced areas are Polygons again and closed coastlines and cliffs are LineStrings. Several fixes change output too, among them (the entries below list every change): get_graph(tags_to_keep=...) keeps the direction tags, so driving graphs are no longer two-way everywhere; crops by a polygon follow the polygon; get_data("europe/georgia") no longer returns the US state; graph exports leave out the edges whose end nodes are missing and take node coordinates from the geometry; and get_data_by_custom_criteria(osm_keys_to_keep=...) returns only the elements with those keys.

  • NEW: OSM.write_pbf shows its progress: one bar “Writing <file>” from 0 to 100 % through reading the file when it is not read yet, applying the edits, preparing the nodes, ways and relations and writing the file. It takes progress=, and by default follows the progress of its OSM object (#633)

  • NEW: find_extracts takes progress= and shows the search, “Finding the extracts”, while it downloads the extract indexes on first use and reads the extract sizes; get_data_by_area passes its progress on (#645)

  • NEW: get_data gains the BBBike cities Damaskus, LosAngeles and Minsk (#665)

  • FIXED: OSM.write_pbf warns when written elements lose the tag their read selected them by, e.g. buildings written without a building column. The warning names the reader that will no longer find them in the written file (get_buildings()); for a frame that no longer notes how it was read (e.g. after pd.merge) it names the tags that pyrosm’s readers detect features by. The file is written as the frame says, as before (#589)

  • FIXED: OSM.write_pbf merges the rows of an element: a change made in one segment of a way read with get_network(nodes=True), or in one of two frames that hold the same element, is written for the whole element, with a UserWarning when other rows of the same frame keep the old value, and rows that give a tag different new values raise ValueError. The last row was written and the other changes were lost. It also no longer writes the network columns u and v as tags, keeps real tags such as length, and writes a tag keyed id back as id instead of id_tag (#590)

  • FIXED: OSM(bounding_box=polygon).to_pbf() crops to the polygon, as a read with it does, instead of the polygon’s bounding rectangle, and so does get_data_by_geocoding; to_pbf uses OSM(workers=...) when the call gives none (#591)

  • FIXED: Downloads and crops never share a file name. The two Geofabrik extracts named georgia-latest.osm.pbf are saved as us-georgia-latest.osm.pbf and europe-georgia-latest.osm.pbf, so get_data("europe/georgia") no longer returns the US state; other names are unchanged. get_data_by_geocoding names its crops area_<place>.osm.pbf: on case-insensitive file systems helsinki.osm.pbf replaced the Helsinki.osm.pbf of get_data("Helsinki") (#592)

  • FIXED: Graph exports leave out the edges whose end nodes are not in the node frame for every graph type: igraph gave the other edges wrong attributes and pandarm failed. After the edge frame is filtered, pandarm and pandana get the same edges and weights as NetworkX; the node x/y of NetworkX, pandarm and pandana graphs come from the node geometry, so projected graphs have projected coordinates; and every graph type identifies the nodes by node_id_col (#604)

  • FIXED: Reading a history file without a timestamp chooses the current time once per OSM object, so later calls reuse the parsed file and the warning appears once; every call parsed the whole file again. keep_other_tags=False on a history read says that history reads do not support it (#608)

  • FIXED: get_graph(tags_to_keep=...) also reads the tags the graph is built from: oneway, junction and, for cycling, oneway:bicycle, and when simplifying the edge_attrs_differ tags. Without them a driving graph was two-way everywhere and the simplification merged edges across changes of speed limit and road class (#611)

  • FIXED: get_data_by_custom_criteria(osm_keys_to_keep=...) without a custom_filter returns the elements with one of the keys instead of every element (#612)

  • FIXED: A crop or merge shows one progress from 0 to 100 % over all its passes, with the current pass in the bar’s label (“Cropping finland-latest.osm.pbf: finding the ways”). The bar was reset for every pass, which restarted its 2-second delay, so it stood at 0 % through passes shorter than that. An open bar is redrawn every second, so its time moves on during a long step (#621)

  • FIXED: Out-of-core reads show their progress through the whole read: indexing the file, decoding it, collecting the elements, building the geometries or edges, writing the result and loading it back, each named in the bar’s label. The bar moved only while the file was decoded (0.8 s of a 6.7 s get_buildings() of Paris), and a read served from the cache showed nothing (#623)

  • FIXED: In-memory and history reads show their progress through the whole read as well: decoding the file, selecting the versions of a history read, the passes that complete the elements a bounding box cuts, collecting the elements and building the ways and relations or the edges. An in-memory get_buildings() of Paris moved the bar during 2.9 of its 17.5 s, and a history read stood at 100 % while it selected the versions (#627)

  • FIXED: get_graph shows one progress from 0 to 100 % over the read and the graph, as a bar “Building the graph of <file>”, and follows the progress of its OSM object: the graph steps followed only pyrosm.enable_logging, so OSM(..., progress=False).get_graph() still drew a bar, and they were a second bar that counted the steps and stood still during a long one. OSM.to_graph, pyrosm.graphs.graph_tables and the graph exporters take progress= (#637)

  • FIXED: A download reports its progress when the server starts sending the file, and then every 256 KiB instead of every MiB, so a slow download shows its bar from the start. get_data, get_data_by_bbox, get_data_by_geocoding, get_data_by_area and find_extracts refuse an invalid progress before any request; get_data returned a file bundled with pyrosm without checking it (#645)

  • FIXED: Crops, merges and OSM.write_pbf log one line each, with their files, output and seconds, as reads do. A bounding-box read served from the out-of-core cache gives the warnings of the read that built it, such as the one about relations that reach beyond the box. A bounding box without data gives the same single warning in both engines; the in-memory engine gave two. The guides turn the progress off with pyrosm.enable_logging("WARNING", progress=False), since enable_logging(progress=False) also turns on the INFO log messages (#646)

  • FIXED: get_data_by_area uses a file it cropped again while the file, the extracts it was cut from and the crop region are unchanged, as a record next to it (<file>.sources) shows; it cropped again on every call, so the out-of-core cache missed and kept one more result per call. update=True and an explicit output_path crop again. With crop=False and one extract, the extract is copied to output_path (a hard link where the file system allows one), which was ignored (#652)

  • FIXED: geocode times out after 60 s by default and takes headers, timeout and opener; it waited without limit for the geocoding service. get_data_by_geocoding checks its options before the request (#657)

  • FIXED: With update=True, find_extracts and get_data_by_area refresh Geofabrik’s index and BBBike’s list of cities as they do Movisda’s indexes: with the network options given, kept next to the downloads, and replaced by the kept copy or pyrosm’s own, with a warning, when they cannot be fetched. Geofabrik’s index was fetched without a timeout or fallback and not kept, and BBBike’s list was never refreshed. pyrosm run --force downloads the extract again, a recipe’s place is geocoded with the headers and timeout of its extract section, and get_data downloads Geofabrik extracts over https (#662)

  • FIXED: OSM.clear_downloads(path) removes exactly the file at path, and only a .osm.pbf or .osh.pbf file; it removed the file of that name in the download folder. Clearing also removes the partial files of interrupted downloads and the records kept beside crops. An unknown dataset name suggests the closest names instead of listing about 800, and a region-qualified name such as usa/new york takes the same spellings as a bare name (#665)

  • FIXED: With complete_relations=True, OSM.to_pbf and OSM.write_pbf write the member ways outside the box of the relations that reach into it, and their nodes, so the written file reads back with the same relation geometries. write_pbf of a read with a bounding box writes only the relations with a member among the nodes and ways read, as to_pbf does; it wrote every relation of the whole file (#668)

  • FIXED: Every function that takes a bounding box takes the same forms: four numbers (a list, tuple or array such as total_bounds), a Shapely Polygon or MultiPolygon, or a GeoDataFrame or GeoSeries, united and reprojected to lon/lat. OSM() refused a tuple, an array and a GeoDataFrame, crops and merges failed on them with unrelated errors, and get_data_by_bbox read a projected GeoDataFrame’s metres as degrees. Crops and merges warn when the written file holds no node (#673)

  • FIXED: OSM.write_pbf writes every element sorted by id and marks the file sorted, so merge_pbf accepts it; new ways came after the source ways. It writes the metadata that was read, node by node, and none where none was read, where ways had changeset 0 and nodes read with keep_metadata=False version 1 and a 1970 timestamp; new elements get version 1 and the time of writing. Crops and write_pbf write next to output_path and replace it only when the file is complete, keeping its permissions, so an interrupted call no longer leaves a truncated file that reads as a smaller valid one (#676)

  • FIXED: simplify_kwargs={"node_attrs_include": [...]} finds node tags as well as node columns, and warns about a name no node has; naming a node tag kept no node (#679)

  • FIXED: With simplify=False, the line of an edge travelled against its way’s node order (the reverse of a two-way street, oneway=-1) runs from u to v; it kept the way’s line (#687)

  • FIXED: In-memory reads with a bounding box and complete_relations=True no longer keep an empty column of a feature that lies outside the box. Errors name the parameter given (extra_attributes, tags_to_keep) instead of tags_as_columns, and every function that takes workers accepts a NumPy integer and refuses other values with one message (#693)

  • FIXED: A closed way that one of its tags makes an area, such as amenity, building, landuse or leisure, is a Polygon also when it has a barrier, highway or route tag. Since 0.23.0 any such tag made it a LineString in every reader unless it was tagged area=yes: in the Helsinki region extract, 274 areas, among them 160 fenced sports pitches and other leisure areas, 65 landuse areas such as fenced allotments, 34 parking lots and other POIs and a fenced school (#704)

  • CHANGED: get_boundaries(name=...) finds the name as plain text, ignoring case, and returns None with a warning when no name contains it. The name was a case-sensitive regular expression, so “Frankfurt (Oder)” was never found and a name with “(” raised an error (#612)

  • CHANGED: Every reader keeps the keys of the filter it selects by as columns, also with tags_to_keep or keep_other_tags=False: get_buildings(tags_to_keep=["name"]) has a building column. get_data_by_custom_criteria takes tags_to_keep as the other readers do, and no reader changes the lists it is given (#616)

  • CHANGED: A plain-dict custom_filter of get_network keeps its matches, as the other forms of a filter and the other readers do; get_network(custom_filter={"highway": ["footway"]}) returned every way but the footways. Pass filter_type="exclude" for that (#618)

  • CHANGED: History reads choose each element’s version from the ids and deletion flags of the ways instead of a table with a column for every tag key of the file: get_buildings(timestamp=...) of the 13 MB Helsinki history sample takes 2.3 s instead of 23.3 s. The keys in the JSON tags column of a history read follow the order of the tags in the file (#629)

  • CHANGED: OSM.write_pbf reads the tags of the frame column by column and copies the nodes as arrays: writing the buildings of the Helsinki extract back takes 17.2 s instead of 32.8 s, and 10.0 s instead of 23.9 s with subset_only=True. The written files are the same (#636)

  • CHANGED: A progress function given to OSM, merge_pbf, OSM.write_pbf, OSM.to_graph, the graph exporters, find_extracts or pyrosm.enable_logging is called as progress(done, 100), with the percentage of the whole operation done, from 0 to 100 and never back. It got the bytes read in each pass over the file of a read, crop or merge, starting from 0 again at each pass, and the steps done of a graph export. Downloads still report progress(written, total) with their bytes (#621, #623, #627, #637)

  • CHANGED: get_data_by_bbox and get_data_by_geocoding are built on get_data_by_area: they choose among Geofabrik, BBBike and Movisda extracts instead of Geofabrik’s alone, choose again after a failed download, and take strategy, must_cover, workers, headers, timeout and opener. They still return a path; with download=False, the URL, or a list of URLs for a set (#657)

  • CHANGED: Crops and merges refuse a bounding box that OSM() refuses and coordinates outside lon/lat; they wrote an empty file. get_data_by_bbox refuses an out-of-range box instead of warning (#673)

  • CHANGED: NetworkX graphs with osmnx_compatible=True (the default) carry oneway and reversed as booleans, as OSMnx gives them, so osmnx.load_graphml reads a saved graph: oneway by the rule the graph is directed by, reversed for an edge against its way’s node order, and on a simplified edge whose ways differ the list of their values. The OSM text of the tag is oneway_tag (#679)

  • CHANGED: Graphs keep their largest connected part before they are simplified, as OSMnx does, so default graphs are fully simplified; a node whose side street the connected-part step removed stayed between two edges of one street (#687)

  • CHANGED: Both engines give the same columns with the same dtypes. visible is a bool column of every read: network, landuse, natural and custom reads had it in the JSON tags. Out-of-core node rows have int32 version, uint32 timestamp and int32 changeset, as in-memory ones do. Results that the out-of-core engine cached with an older version are not reused (#693)

  • CHANGED: Whether a closed way is a Polygon follows the OSM wiki’s list of polygon features, as in OSMnx: it is an area when it is not tagged area=no and one of its tags is an area tag (any value of amenity, building, landuse, leisure and the other area keys, some values of barrier, highway, natural, railway, waterway and a few other keys), and otherwise a LineString. Closed coastlines, cliffs, tree rows, streams, railway loops and ways without an area tag are LineStrings, where they were Polygons; closed hedges, city walls, retaining walls and motorway service areas are Polygons, where they were LineStrings. Results that the out-of-core engine cached with an older version are not reused (#704)

  • DOCS: The readers of pyrosm.engine are documented as the out-of-core engine’s internals behind OSM, without an API promise: they do not check their parameters and they read every version of a history file. Read data with OSM (#693)

  • DOCS: The OSM.write_pbf example runs, the guide to custom filters names extra_attributes and get_data_by_custom_criteria and says what a filter does with each reader, the graphs guide says that cycling graphs follow the one-way tags, the guide to reading OSM data shows network ways as LineStrings and says a bounding box keeps the features with a node inside it, and the get_pois docstring lists boundary among the keys of Conf.tags.available (#694)

v0.23.0 (Oct 19, 2026)#

This release runs out-of-core reads, crops and merges on threads instead of worker processes, so parallel reads need no if __name__ == "__main__": guard, and builds their results in C on those threads: on Finland (737 MB), get_network("driving") takes 4.1 s on 8 threads, against 19.8 s in one process with 0.22.0. They default to workers="auto", which reads files from about 20 MB in parallel, and no longer warn about that change. The worker-process backend (PYROSM_DECODE_BACKEND=processes) is deprecated. Several fixes change results: a missing visible flag reads as True (every node and way of a file that is not a history file had visible=False) and relations have a visible value; closed ways are lines or polygons by all their tags, also those that are not columns; a node’s or relation’s tag keyed id comes out as id_tag instead of replacing its id; filters read that tag as id_tag in every reader, while the filter key id raises a ValueError; a custom_filter narrows get_boundaries, get_buildings, get_landuse and get_natural instead of widening them; and driving networks leave out busways, ladders and trails. Out-of-core results cached by earlier versions are read again.

  • NEW: Graph simplification takes OSMnx’s edge_attr_aggs (in simplify_kwargs of to_graph, and in simplify_graph): a dict of column -> function, called with the list of the merged segments’ values, e.g. {"travel_time_s": sum}. By default a travel_time column is summed, as in OSMnx; length and the pandana weights stay summed (#575)

  • FIXED: Closed ways are polygons or lines by all their tags. A closed way is a Polygon unless it is tagged area=no, or, without area=yes, it has a highway, barrier or route tag; the readers read these tags only from the read’s columns, so get_buildings(), whose columns lack area, returned a building tagged area=no as a Polygon (#553)

  • FIXED: A relation’s tag keyed id is kept as id_tag, as a way’s is: a column when requested (extra_attributes=["id_tag"]), otherwise a key of the JSON tags. It replaced the relation’s OSM id in every reader (#554)

  • FIXED: A node’s tag keyed id is kept as id_tag, as a way’s is. Requesting id as a column (extra_attributes=["id"]) replaced the node ids with the tag’s values, and out of core a read of nodes and ways then failed with ArrowTypeError; extra_attributes=["id_tag"] gave no column, and the JSON tags key was id (#555)

  • FIXED: Out-of-core reads take a requested tag key that UTF-8 cannot encode, such as a lone surrogate, as the in-memory reader does: no tag has it, so its column is absent and a filter by it keeps nothing. They raised UnicodeEncodeError (#556)

  • FIXED: The raw block decoder sums the delta-coded refs of ways and member ids of relations with wrap-around, as numpy’s int64 arithmetic and the batch decoder do; a sum past the int64 range was a signed overflow, which C leaves undefined (#557)

  • FIXED: A missing visible flag reads as True, as the PBF format reads it: every node and way of a file that is not a history file had visible=False in every reader (on the Helsinki region extract, the 1613 nodes of get_pois() and the 423 ways of get_buildings()). Relations have a visible value too, a column when requested and a key of the JSON tags otherwise. History files, which write the flag, read as before (#558)

  • FIXED: Filters read a tag keyed id as id_tag, the column it comes out in, for nodes, ways and relations in every reader, also in bracket filters, osm_keys_to_keep and network filters; they differed by reader (out of core {"id_tag": True} kept nothing, in memory {"id": True} kept every tagged way). The filter key id, the element’s OSM id, raises a ValueError that names id_tag. With keep_other_tags=False, a requested id_tag column is returned (#559)

  • FIXED: A pandarm export (to_graph(..., graph_type="pandarm") or pyrosm.graphs.to_pandarm) gives pandarm the CRS of the nodes. pandarm warned on every export that no CRS was passed and assumed geographic coordinates (#564)

  • FIXED: The check that the timestamp of a history-file read is not in the future takes the current time with pd.Timestamp.now("UTC"). It used pd.Timestamp.utcnow(), which pandas 3 deprecates and will remove (#565)

  • FIXED: OSM.list_cache and OSM.clear_cache take the bare name of a file that get_data downloaded, as OSM.clear_downloads does. A bare name was read only as a path in the working directory, so OSM.clear_cache("finland-latest.osm.pbf") matched no cached layer and removed nothing (#569)

  • FIXED: get_boundaries, get_buildings, get_landuse and get_natural narrow their result by a custom_filter, as their docstrings say: every part of the filter without the layer key also requires it, and get_boundaries keeps boundary_type unless the filter names boundary. The layer key was added as an OR term, so a filter on another key widened the result: get_boundaries(custom_filter={"admin_level": ["10"]}) returned every boundary=* feature plus everything with admin_level=10 (#571)

  • FIXED: OSM.to_pbf, crop_pbf and merge_pbf take workers="auto", choosing the worker count as out-of-core reads do; they raised TypeError. They also validate workers as reads do (#574)

  • FIXED: pyrosm.geocode and get_data_by_geocoding fetch the Nominatim reply again when it fails part way, as downloads do; a reply cut off mid-transfer raised a bare http.client.IncompleteRead (#579)

  • FIXED: workers="auto" and the cap on an explicit workers count the CPU cores the process may use (os.process_cpu_count() on Python 3.13+, the CPU affinity on Linux). They counted every core of the machine, so in a job limited to 4 cores of a 64-core node a read of a large file started 62 threads (#583)

  • FIXED: get_data_by_area takes workers="auto", as crop_pbf and merge_pbf do; it raised ValueError (#584)

  • CHANGED: Out-of-core reads, crops and merges run in parallel on threads instead of worker processes, so with workers > 1 they need no if __name__ == "__main__": guard on macOS and Windows. The blobs are decompressed in batches and decoded by a C decoder with the GIL released, and crops write their blocks with a C writer; a block either one does not handle goes to the existing code, so results and crop output bytes are unchanged. On Finland (737 MB), get_network("driving") takes 14.6 s on one thread instead of 19.8 s in one process, and 9.8 s on 8 threads; a box crop takes 5.7 s on 8 threads instead of 17.5 s on one. Setting the environment variable PYROSM_DECODE_BACKEND=processes brings back the worker processes (#525, #526, #527, #528, #529, #530, #531, #532)

  • CHANGED: Out-of-core reads on threads collect and assemble their ways and relations in less time. The threaded decode keeps each element’s tags as string ids, and the ways a read outputs are selected, filtered by value and given their tag columns, JSON tags column and geometries in C on the read’s threads; network edges are built as Arrow tables, and relations are assembled with vectorized Shapely calls. The results are unchanged. On Finland (737 MB), get_network("driving") takes 4.1 s on 8 threads instead of 9.8 s and 11.3 s on one instead of 14.6 s, and get_buildings() 9.4 s on 8 threads instead of 34.0 s and 17.3 s on one instead of 39.5 s (#536, #537, #538, #539, #540, #541, #542, #543, #544, #545, #546)

  • CHANGED: Out-of-core reads on threads build their point features (the tagged nodes a layer read returns, as in get_pois()) from string ids: the nodes are filtered by value and given their tag columns and JSON tags column in C, as the ways are, and written as Arrow tables. The results are unchanged. On the Helsinki region extract (37 MB), get_pois() takes 0.62 s instead of 0.81 s (#548)

  • CHANGED: Selecting the worker-process backend with PYROSM_DECODE_BACKEND=processes gives a FutureWarning, once per process: the backend is deprecated and will be removed in a future release. Reads, crops and merges run on threads by default (#551)

  • CHANGED: get_network("driving") and "driving+service" leave out highway=busway, ladder and trail ways, which cars cannot use; OSMnx’s drive keeps them (#572)

  • CHANGED: Reads, crops and merges that leave workers unset run with workers="auto": one thread below about 20 MB of source data, more threads for larger files. They ran on one core, and the FutureWarning of 0.22.0 that pyrosm 1.0 would run them in parallel by default is removed. Results and written files are the same for every worker count. With PYROSM_DECODE_BACKEND=processes the default stays one process (#573, #584)

  • CHANGED: Simplifying a graph warns when a numeric column that differs along a merged edge becomes lists, naming the column and the edge_attr_aggs that would aggregate it (#576)

  • CHANGED: workers="auto" reads files of about 20 to 70 MB on up to 4 threads; it read every file below 70 MB on one thread. Smaller files stay on one thread, and larger files use the CPU cores less two, as before. On a 10-core laptop, a 34.5 MB file reads in 16 to 22 % less time on 4 threads than on 1 (#580)

  • DOCS: The docstrings of OSM (its workers parameter), OSM.to_pbf, get_data_by_area and the out-of-core engine say that parallel reads, crops and merges run on threads, which need no if __name__ == "__main__": guard, and that only PYROSM_DECODE_BACKEND=processes uses worker processes that need it on macOS and Windows (#549)

  • DOCS: The guide to reading OSM data and the FAQ explain how reads, crops and merges choose their number of threads, and that workers=1 keeps them on one core. The list in the 0.2.0 notes of the changelog page shows as one list again (#524, #584)

  • DOCS: The code examples of the guides run on real data and show their output, and the guide notebooks are re-run with this release (#563, #567, #568, #577, #581, #585)

v0.22.0 (Oct 18, 2026)#

This release adds OSM.get_graph, which reads a street network as a routable graph in less memory: on Finland’s driving network (one process), exporting to NetworkX peaks at about 4 GB and takes 41 s, against 13.6 GB and 83 s for get_network(nodes=True) followed by to_graph. Graph exports run faster, take less memory and show their progress, and pyrosm.enable_logging(progress=...) sets the progress of every read, crop, merge and download. get_natural() on Finland takes 77 s instead of 72 min. pyrosm supports Python 3.11–3.15; Python 3.10 is no longer supported. Two changes alter results: NetworkX graphs leave out attributes that have no value instead of storing NaN or None, and nodes whose tags all became columns have no tags value. get_path() and the pandana graph backend warn that they will be removed in pyrosm 1.0, and reads, crops and merges that leave workers unset warn that pyrosm 1.0 runs them in parallel by default.

  • NEW: pyrosm.enable_logging(progress=...) sets the progress of every read, crop, merge and download whose own progress is not given: True (as without the call) shows the bars, False shows none, and a function is called as progress(done, total). It applies to OSM objects made before the call too. The progress of OSM, crop_pbf, merge_pbf, get_data and the other download functions defaults to None, which follows this setting, and still overrides it for one object or call (#502)

  • NEW: Graph exports show their progress as pyrosm.enable_logging(progress=...) sets it. OSM.to_graph and pyrosm.graphs.to_networkx, to_igraph, to_pandana, to_pandarm and graph_tables draw a bar of steps that names the current one (directing the edges, simplifying, keeping the largest connected part, building the graph), shown after 2 seconds and cleared at the end, or without a terminal one line with the time it took; a function set with enable_logging is called with the steps done and the steps (#504)

  • NEW: OSM.get_graph(network_type, graph_type, ...) reads a street network as a routable graph, the graph of OSM.to_graph(*osm.get_network(network_type, nodes=True), graph_type, ...), and takes the parameters of both. With the out-of-core engine it builds the simplified graph from the ways and nodes of the read, without the tables of every street segment and every node; with simplify=False, the in-memory engine or a history read it calls get_network and to_graph. On Finland’s driving network (one process), exporting to NetworkX peaks at about 4 GB of memory (3.7-4.5 GB over several runs) and takes 41 s, against 13.6 GB and 83 s for get_network(nodes=True) followed by to_graph (#509, #510)

  • FIXED: The progress given to get_data_by_area, get_data_by_bbox or get_data_by_geocoding covers their crop or merge too; it covered only the download, so progress=False still showed the crop’s bar (#502)

  • FIXED: crop_pbf and OSM.to_pbf refuse an output path that is their source file, under the same name or through a link, with a ValueError, as merge_pbf does; they truncated the source (#503)

  • FIXED: get_data_by_custom_criteria, get_pois and the other readers that take a custom_filter leave the caller’s dict as given; a key’s True became [True]. Nodes whose tags all became columns have no tags value, as other elements without leftover tags: both readers kept each such node’s tag dict there, and the out-of-core reader raised ArrowTypeError when those nodes came with ways or relations that have leftover tags (#516, #517)

  • FIXED: The readers refuse a data blob that decompresses to more than the 32 MiB the PBF format allows, with InvalidOSMFileError, as crops and merges do; they decompressed it whole, however large, so a small file could take all the memory. This holds for zlib blobs without a raw_size or with a wrong one, which are still read, and for LZMA blobs (#519)

  • FIXED: Out-of-core reads that decode at the same time in one process, each in its own thread, keep their own settings. A read that decodes in the calling process (workers=1, the default; workers="auto" on files under 70 MB; the fallback to one process) kept its file, filter keys, bounding box and shard folder in variables the whole process shares, so a read that started while another was decoding changed them: the other read returned too few rows without an error, read the wrong file or wrote its shards to a deleted folder (#520)

  • CHANGED: get_path() and the pandana graph backend (graph_type="pandana" and pyrosm.graphs.to_pandana) warn with a FutureWarning, which Python shows by default, that they will be removed in pyrosm 1.0; they warned with a DeprecationWarning, which Python shows only for calls made directly from the script it runs, and to_pandana did not warn. A read, crop or merge that leaves workers unset warns once per process that pyrosm 1.0 will run reads, crops and merges in parallel by default; passing workers=1 keeps them on one core and silences it (#521)

  • CHANGED: to_graph(graph_type="networkx") and pyrosm.graphs.to_networkx build the graph in less time. The node and edge attributes are read from whole columns instead of row by row, and the edges whose end node is missing and the keys of parallel edges are found with column operations instead of a Python loop. On Finland’s driving network (one process), building the graph takes 7.8 s instead of 14.6 s, and to_graph 47 s instead of 60 s; the graph is the same (#501)

  • CHANGED: Graph exports simplify the graph in less time. Each node’s distinct neighbours are counted with a hash of node pairs instead of a sort, the nodes where maxspeed or highway changes are found from each column’s lowest and highest code, and the other columns of a merged edge are compared only where neighbouring segments both have a value. On Finland’s driving network (one process) simplifying takes 14 s instead of 28 s, and to_graph 33 s instead of 48 s; the graph is the same (#505)

  • CHANGED: Graph exports that simplify the graph (the default of to_graph and pyrosm.graphs.graph_tables) take less memory and time. The directed edges are row numbers of the edges with a direction each instead of a copy of the edge table, which repeated every column for both directions of a two-way street, and only the simplified edges get rows of their own. On Finland’s driving network (one process) the graph tables peak at 13.2 GB instead of 17.2 GB of memory and take 15 s instead of 20 s; the graph is the same (#507)

  • CHANGED: Graph simplification takes less memory. Neighbouring segments are compared by the codes of each column instead of their values, merged columns hold each distinct string once, the working indices are 32-bit when they fit (OSM ids stay 64-bit), and working arrays are freed after their last use; get_graph frees the arrays of its read once the simplified tables exist. The graph is the same (#508, #511)

  • CHANGED: NetworkX graphs (to_graph(graph_type="networkx"), pyrosm.graphs.to_networkx and OSM.get_graph) store only the attributes an edge or node has a value for, as OSMnx’s graphs do: an attribute without a value is left out instead of being stored as NaN or None. Equal strings are stored once (#512)

  • CHANGED: get_network(nodes=True) and OSM.get_graph gather the tags and metadata of the network’s nodes in less time and memory. The second pass over the file tests each block’s node ids against a hash set of the wanted ids in one call, instead of each node against a Python set, and keeps the node tags as string ids, as the in-memory reader does, instead of one dict per node. On Finland’s driving network (one process) the pass takes 8 s instead of 20 s and get_graph’s read peaks at 2.3 GB instead of 3.3 GB; the tables are the same (#514)

  • CHANGED: Multipolygon relations with many rings are assembled in less time. The even-odd overlay of a relation’s rings is taken in pairs, round by round, instead of adding each ring in turn to the area built so far, which made the time grow with the square of the ring count. On Finland (one process), get_natural() takes 77 s instead of 72 min; its largest lake, Saimaa, has 9,489 rings. The geometries are the same; the rings and vertices of a multipolygon can come in another order (#515)

  • CHANGED: pyrosm supports Python 3.11–3.15. Wheels are published for Python 3.15, and the tests run on it. Python 3.10, whose support ended in October 2026, is no longer supported; on Python 3.10, pip installs the last release that supports it (#522)

v0.21.0 (Oct 15, 2026)#

This release crops PBF files several times faster and merges them in about half the time: on Finland (771 MB, one process), a crop of the Helsinki region takes about 15 s instead of 100-111 s, and 16-17 s instead of 104-122 s with repack=True. Crops and merges show progress, and pyrosm decompresses and compresses PBF blocks with libdeflate, compiled in from vendor/libdeflate, which speeds up reads too. The raw block decoder rejects corrupt blocks instead of reading past their data.

  • NEW: Cropping and merging show progress. crop_pbf and merge_pbf take progress as OSM does, and OSM.to_pbf uses the progress given to OSM(...): by default a bar on stderr, “Cropping <file>” or “Merging <files>”, once the work has run 2 seconds, cleared at the end, or without a terminal one line with the time it took. A function is called as progress(done, total) with the bytes of the inputs read, from (0, total) to (total, total) for each pass over them: a crop makes two passes or more, and a merge one, or more with a bounding box or polygon (#496)

  • FIXED: The raw block decoder, which every reader and the crop use, raises InvalidOSMFileError for a corrupt block instead of reading or writing past the block’s data: a field or a number that does not fit in its data, a field number protobuf does not allow, ways or relations whose keys and values (or member ids, types and roles) differ in number, and dense node groups whose coordinates, metadata or tag stream do not match their ids. A packed field written in several runs is read whole, as protobuf reads it (#483)

  • FIXED: The raw block decoder reads a way or relation without its id, and a plain node without a coordinate, as 0, the protobuf default, instead of leaving the value undefined (#491)

  • CHANGED: crop_pbf and OSM.to_pbf crop faster. The selection decodes blocks with the raw block decoder and tests the elements of a block together, reading a file whose blocks hold nodes, then ways, then relations once instead of three times. The write pass copies the kept ways and relations as they are stored and encodes the kept dense nodes from arrays instead of rebuilding each block through protobuf messages. On Finland (771 MB, one process) a crop of the Helsinki region takes 28-33 s instead of 100-111 s, and 53-61 s instead of 104-122 s with repack=True; the output is the same byte for byte (#484, #485)

  • CHANGED: Re-packed crops (repack=True) and merge_pbf read their input faster. They find the dense nodes to keep in the arrays of the raw block decoder and parse with protobuf only the plain nodes, ways and relations they keep, instead of parsing every block and testing each element in Python. On Finland (771 MB, one process) a re-packed crop of the Helsinki region takes 36-37 s instead of 55-56 s; the output is the same byte for byte (#487)

  • CHANGED: Crops (crop_pbf, OSM.to_pbf) and merge_pbf with a box find the distinct ids they keep with a hash set instead of sorting them with np.unique. On Finland (771 MB, one process) a crop of the Helsinki region takes 25-26 s instead of 31-32 s, and 31-33 s instead of 37-38 s with repack=True; the output is the same byte for byte (#488)

  • CHANGED: Crops and merge_pbf take less time. Each block’s node ids are tested against the sorted kept ids in one walk, other id lookups load their hash buckets ahead, and re-packed blocks (repack=True and merge_pbf) are encoded from arrays instead of protobuf messages, with the kept ways and relations decoded by the raw block decoder. On Finland (771 MB, one process) a crop of the Helsinki region takes 21-22 s instead of 24-26 s, and 23-24 s instead of 32-33 s with repack=True; merging two Finland-derived files takes 9 s instead of 18-19 s. The output is the same byte for byte (#489, #490, #491)

  • CHANGED: pyrosm decompresses the blocks of a PBF with libdeflate, which is compiled into pyrosm from vendor/libdeflate (release 1.26, MIT licence) and is about 2.5 times as fast as Python’s zlib; a block that does not declare its size is still read with zlib. On Finland (771 MB, one process) a crop of the Helsinki region takes 16 s instead of 21-22 s, a re-packed crop 17-18 s instead of 22 s, and an in-memory read with get_boundaries() 16-17 s instead of 19-20 s; the results are the same (#494)

  • CHANGED: Crops, merge_pbf and write_pbf compress the blocks they write with the vendored libdeflate instead of Python’s zlib, at the same level (6). The files hold the same data in slightly fewer bytes, so they no longer match earlier versions byte for byte. The SHA-256 of a cropped or merged extract (AreaExtract.sha256) changes accordingly, so a recipe whose extract.sha256 was pinned with an earlier version stops until the pin is updated. On Finland (771 MB, one process) a crop of the Helsinki region takes 15.4-15.5 s instead of 16.9-17.6 s and is 85.3 MB instead of 86.8 MB, and a re-packed crop takes 16.4-16.8 s instead of 17.3-18.0 s and is 60.9 MB instead of 61.7 MB (#495)

  • DOCS: The menu button of the documentation on phones opens the navigation sidebar again. The documentation is built with sphinx-book-theme 1.2.0, since its later versions use pydata-sphinx-theme 0.17-0.20, whose mobile sidebar button does nothing (#482)

v0.20.0 (Oct 12, 2026)#

This release removes the dependency on cykhash, so pip installs pyrosm’s wheels on glibc-based Linux, macOS and Windows on x86_64 without compiling anything, and the installation guide puts pip first.

  • CHANGED: pyrosm no longer depends on cykhash, which PyPI offers only as source code, so pip installs pyrosm’s wheels without compiling anything. The sets of OSM ids and the map from node id to row that pyrosm uses are now its own, built on klib’s khash.h, the hash table cykhash uses too. Reads return the same results; on Finland’s 95 million node ids, building and querying these tables takes 10-40 % less time (#478)

  • DOCS: The installation guide, the README and the FAQ put pip install pyrosm first and list the platforms with wheels (CPython 3.10–3.14 on glibc-based Linux on x86_64 and aarch64, macOS, and Windows on x86_64). The advice to install GeoPandas first on Windows and the list of dependencies that needed installing beforehand, which still named cykhash, are removed (#479)

v0.19.1 (Oct 12, 2026)#

This release makes the predefined networks follow OSM’s access rules: a way that access=no closes to the public is left out unless a tag of the network’s mode of transport allows the use.

  • FIXED: The predefined networks no longer include ways that access=no closes to the public, unless a tag of the network’s mode of transport allows the use. The most specific access tag present decides, as in OSM’s access hierarchy (walking: foot, access; cycling: bicycle, vehicle, access; driving: motorcar, motor_vehicle, vehicle, access), and a way is left out when that tag is no or private: access=no with foot=yes stays in the walking network, access=private with foot=yes is now kept, and all_public leaves out access=no as well. In the walking network of central Helsinki, the Suomenlinna maintenance tunnel is no longer included, so the island is no longer joined to the mainland (#474)

v0.19.0 (Oct 12, 2026)#

This release reads with the out-of-core engine by default and makes both readers faster and leaner: OSM(...) reads country-sized files with bounded memory, and both engines return the same columns in the same order. get_network() returns each edge as one LineString and to_graph simplifies the graph by default. It also fixes history reads that returned deleted elements, graphs that took the network type of the last read, and out-of-core reads of files whose nodes are not in the dense format.

  • CHANGED: get_network() returns each edge as one LineString through its way’s nodes instead of a MultiLineString of two-point segments, and the other layers return each line way (an open way, or a closed way with a highway, barrier or route tag or area=no) as one LineString too; relations keep their MultiLineStrings. The coordinates are the same and in the same order, and the length column keeps its values. Results that the out-of-core engine cached with an older version are not reused (#446, #447)

  • CHANGED: to_graph simplifies the graph by default, and so do pyrosm.graphs.to_networkx, to_igraph, to_pandana, to_pandarm and graph_tables, which the graph stage of recipes uses. A chain is no longer merged across a change of maxspeed or highway (a missing value next to a set one counts as a change), so each edge has one speed limit and one road class, and pandana and pandarm graphs sum their weight columns along a merged chain like length. simplify=False keeps one edge per segment, and simplify_kwargs={"edge_attrs_differ": None} merges as OSMnx does by default (#449)

  • CHANGED: Reading networks and line layers takes less time and memory. On Finland (737 MB, one process), get_network("all") takes 45-46 s instead of 90-101 s with the out-of-core engine and peaks at 7.5-8.4 GB instead of 14.9 GB of memory, and takes 102 s instead of 206 s with the in-memory engine, peaking at 28.4 GB instead of 40.4 GB. The in-memory parse joins its per-block arrays without a Python object per value, polygon orientation returns points and lines as they are instead of copying them, lines and the edges of get_network(nodes=True) are built in batches, and the out-of-core decode of a network no longer writes every way’s node refs to disk (#444, #445, #446, #447, #448, #450)

  • CHANGED: OSM(...) reads with the out-of-core engine by default. It decodes the file in a streaming pass with bounded memory and caches each read’s result in the temporary folder, so an identical read returns it without decoding the file again (OSM.clear_cache() removes the cached results). workers=None reads on one core without a warning; workers="auto" decodes in parallel. OSM(..., engine="in_memory") keeps the previous behaviour, and history reads (an .osh.pbf file or a timestamp) and write_pbf use the in-memory reader with either engine. The two engines return the same elements and values, but the order of rows and the integer width of some metadata columns can differ (#460)

  • CHANGED: Out-of-core reads take less time and memory. On Finland (737 MB, one process), get_network("all") takes 36 s instead of 48 s and peaks at 6.8 GB instead of 7.7 GB, and get_buildings() takes 43 s instead of 53 s and peaks at 5.0 GB instead of 6.0 GB. The node coordinates a read needs are looked up within each spilled block’s id range (0.4 s instead of 5.8 s for get_pois()), a block’s tags are resolved through its string table decoded once, and relations find their member ways through an index instead of scanning all of them for each relation (21 s instead of 30 s to assemble get_landuse()), which the in-memory reader uses too (#454, #455, #456)

  • CHANGED: Parallel out-of-core reads (workers="auto" or workers=N) decode the file in about eight tasks per worker, each with a similar share of the file’s bytes, which the workers take as they become free; each worker used to decode one equal share of the file’s blocks, and the last share held all the ways. The ways and node coordinates a read keeps are collected in the main process, as in a one-process read, instead of in two more process pools, which were slower than one process and raised its memory peak. workers="auto" uses the CPU cores less two, up to eight, instead of every core (#462)

  • CHANGED: The in-memory reader (engine="in_memory", history reads and write_pbf) reads faster with less memory. It decodes the file with the raw block decoder the out-of-core engine uses instead of protobuf messages, keeps ways and node tags as arrays and builds their dicts only for the elements a read can keep, and keeps the node columns without a DataFrame copy. On Finland (737 MB) the parse takes 15 s instead of 81 s, peaks at 11.2 GB instead of 28.5 GB and keeps 8.3 GB instead of 19.9 GB in memory; get_network("all") after it takes 18 s instead of 23 s and get_pois() 4 s instead of 13 s. Blobs stored raw or lzma-compressed are now read, untagged nodes of plain (non-dense) node groups get None tags like dense ones instead of an empty dict, and parse_osm_data no longer warns about protobuf’s pure-Python backend (#463, #464, #466, #467, #468)

  • CHANGED: Reads build their result in less time. The way columns are no longer copied to drop the ways without nodes when every way has nodes, the in-memory reader puts the tags of the ways into their columns in one pass, and the out-of-core engine writes its chunks of ways straight into the result instead of writing each to a temporary file first. On Finland (737 MB, one process), reading all highway ways with get_data_by_custom_criteria takes 27 s instead of 34 s with the in-memory engine and 29 s instead of 35 s with the out-of-core engine, and get_buildings() takes 34 s and 36 s instead of 44 s with the two engines. The in-memory engine peaks 0.6-0.9 GB lower; the peak of the out-of-core engine does not change (#469, #470, #471)

  • CHANGED: The out-of-core engine returns the columns in the order of the in-memory reader: timestamp before version, visible (a default column of get_buildings()) and nodes (with keep_node_info=True) at their place among the other columns, tags after lat in the node frame of get_network(nodes=True), and a tag that first occurs after the first 250,000 ways of a result at its place instead of at the end. Results that the out-of-core engine cached with an older version are not reused (#471)

  • FIXED: History reads (an .osh.pbf file, or a timestamp) no longer return elements that were deleted before the timestamp: a deleted element used to come back as its last version before the deletion. With a bounding_box, they no longer return elements that had left the box by then, at their old position: the box is now applied to each element’s state at the timestamp, not to every version. On the Helsinki test history file at 2010-01-01, three buildings and 33 network ways that had been deleted are gone (#443)

  • FIXED: to_graph takes the network type from the frames it is given. get_network recorded the type in a list shared by every GeoDataFrame in the process, so a graph got the type of the network read last: driving edges exported after a walking read became a two-way graph. Out-of-core frames recorded no type, so to_graph without network_type failed. Each frame now keeps its type in attrs["network_type"]; a given network_type takes precedence, and an unknown one raises a ValueError (#452)

  • FIXED: The out-of-core engine reads PBF files whose nodes are not in the dense format (for example written by osmium with pbf_dense_nodes=false); it returned nothing for them (#453)

  • FIXED: The out-of-core engine keeps each way’s nodes column with keep_node_info=True, which write_pbf(..., apply_geometry=True) needs to reshape ways; it dropped the column (#457)

  • FIXED: get_data_by_custom_criteria(custom_filter=None) reads every tagged element with the out-of-core engine too; it raised NotImplementedError (#458)

  • FIXED: Out-of-core reads that find nothing warn as in-memory reads do, and get_data_by_custom_criteria raises a ValueError for a non-string filter_type with either engine (#459)

v0.18.0 (Oct 10, 2026)#

This release adds recipes, YAML files that describe how the OSM data of an analysis is made and that pyrosm run carries out with a provenance record, and pyrosm.enable_logging() to show pyrosm’s log messages. It also fixes the column order of node layers, tags lost when write_pbf gets a frame without tag columns, and history reads that mixed values from different versions.

  • NEW: Recipes. A recipe is a YAML file that says how the OSM data of an analysis is made: an extract to download (by bounding box, place name, area file or dataset name), layers to read from it, a graph to build, and where each output goes. pyrosm run RECIPE [-o DIR] (or pyrosm.recipes.run) writes the extract as PBF, each layer and the graph’s node and edge tables as GeoParquet, and a provenance record with the package versions, the resolved recipe, the SHA-256 of every input and output and where the extract came from. Outputs are moved into place only when every stage has succeeded; a rerun reuses the stages that did not change, and sha256 and snapshot pins refuse data that differs. pyrosm validate RECIPE checks a recipe without running it, and pyrosm.graphs.graph_tables returns a graph’s node and edge tables as the graph exporters build them. Recipes need PyYAML and pyarrow (pip install "pyrosm[recipes]"), and the user guide has a page on them (#432, #433, #434, #435, #436, #437)

  • NEW: pyrosm.enable_logging(level="INFO", to_file=None) shows pyrosm’s log messages on stderr or in a file. At INFO each OSM object logs its options, and each read (get_network, get_buildings, …, to_graph) logs one line with its arguments, the number of features and the seconds it took; DEBUG adds the time of reading the PBF, the out-of-core engine’s phases and its cache hits and misses. Messages are op key=value ... lines, and the existing download, geocoding and extract messages now use the same form (#431)

  • FIXED: get_pois, get_natural and get_data_by_custom_criteria return their columns in the same order on every run. The node columns (id, lon, lat, tags, visible, version, timestamp, changeset) came in an order that changed with Python’s hash seed, so the same read could give differently ordered frames and files with different SHA-256 from run to run. They now come in the order the out-of-core engine uses (#439)

  • FIXED: write_pbf keeps the tags of the elements a frame matches when the frame has no tag columns, such as one that names elements by id and osm_type only. Such a frame used to strip their tags, so subset_only=True wrote valid but tagless elements (#440, #383)

  • FIXED: History reads (an .osh.pbf file, or a timestamp) take each element’s latest version whole. A tag that the latest version had removed used to come back from an older version, so a row could mix two versions. On the Helsinki test history file at 2010-01-01, three ways that had lost their building tag are no longer buildings, and one way that had lost its highway tag leaves the network (#441, #379)

v0.17.0 (Oct 9, 2026)#

This release shows progress while pyrosm reads a PBF, and fixes a failure of get_mask_by_osmid on NumPy 1.24–1.26 and of NodeLocations.gather on an empty coordinate store.

  • NEW: Reading a PBF shows progress. OSM(..., progress=True) (the default) shows a bar on stderr once a read has run 2 seconds, a widget in Jupyter when ipywidgets is installed, and clears it when the read finishes; without a terminal, a read that took 2 seconds or more prints one line with the time it took. progress=False shows nothing, and a function is called as progress(done, total) with the bytes of the file read so far, from (0, total) to (total, total) for each pass over the file. Both the in-memory reader and the out-of-core engine report it, including the second pass of get_network(nodes=True); the pyrosm.engine readers take progress too (#426, #202)

  • FIXED: pyrosm.data_filter.get_mask_by_osmid, which pyrosm.frames.create_nodes_gdf uses to filter nodes by osmids_to_keep, no longer fails with AttributeError: module 'numpy' has no attribute 'bool' on NumPy 1.24–1.26 (#427, #377)

  • FIXED: NodeLocations.gather, which looks up node coordinates while geometries are built, returns -1 (absent) and NaN coordinates for every id when no node coordinates were read, instead of raising IndexError. Ids that are absent from a non-empty store now get NaN coordinates instead of those of the store’s first node (#428, #378)

v0.16.0 (Oct 9, 2026)#

This release shows the progress of downloads in terminals and in Jupyter, and lets a calling library draw its own progress bar. pyrosm now requires tqdm.

  • NEW: Downloads show a progress bar on stderr, drawn as a widget in Jupyter when ipywidgets is installed (pip install "pyrosm[notebook]") and as a text bar elsewhere. Without a terminal, as in logs and CI, only a line naming the file is printed. get_data, get_data_by_area, get_data_by_bbox, get_data_by_geocoding and download accept progress: True (default), False for no output, or a function called as progress(written, total) after each chunk, so a calling library can draw its own bar. pyrosm now requires tqdm (#424)

  • FIXED: The test suite runs from the source package. test_page_size_parser tests a maintainer script that the source package does not include; it is now skipped when the script is missing instead of failing with FileNotFoundError (#423)

Thanks for all the contributors who helped to improve the library either via PRs, or by raising or participating in an issue:

v0.15.0 (Oct 1, 2026)#

This release extends get_data_by_area for callers that fetch the data of many areas: it can list the candidate extracts without downloading, merge a smaller set of extracts, require coverage only of what matters, crop to the area’s polygon with several processes, and record the hash and snapshot time of the file it returns. Downloads are retried and resumed, and accept the caller’s headers, timeouts and opener.

  • NEW: Add pyrosm.find_extracts(area, contains_only=False, update=False, directory=None) to list the Geofabrik, BBBike and Movisda extracts that overlap an area, with their download sizes and whether each contains the whole area, without downloading any of them. Extracts that contain the area come first, smallest download first. get_data_by_area takes its candidates from it (#401)

  • NEW: get_data_by_area(..., strategy="smallest_total") covers an area with several extracts and merges them with merge_pbf when their total download is smaller than the smallest extract that contains the area, or when that extract’s size is unknown. The set is chosen greedily (lowest size per area newly covered) from Geofabrik, BBBike and Movisda administrative extracts, with at most one Movisda extract, which goes last in the merge; Movisda grid tiles are never combined. The default, strategy="single", never merges. AreaExtract.sources lists the downloaded extracts (#408)

  • NEW: find_extracts and get_data_by_area accept must_cover, a geometry that an extract must cover in place of the whole area, such as the transit stops routing needs, so sea or unserved edges of the area no longer force a larger extract. Points and lines are covered exactly, polygons except for less than 1 m². The crop still follows the area (#409)

  • NEW: AreaExtract records sha256, the SHA-256 of the returned file, and snapshot, the data time from the PBF header’s osmosis_replication_timestamp (for a merged file the oldest of its sources), and each record in sources carries its own. They are read after the file is written, and read again when a file changes meanwhile (#410)

  • NEW: get_data_by_area(..., crop="polygon") crops the extract to the area itself instead of its bounding box, and merge_pbf(..., polygon=...) crops one or several inputs to a polygon. A node is kept when it lies inside the polygon or on its boundary, and a way with a kept node is kept whole (#411)

  • NEW: get_data_by_area(..., workers=N) crops and merges with N worker processes, and writes the same file as one process. Cropping the 64 MB Helsinki test extract to a city took 6.6 s with one worker, 3.9 s with two and 2.5 s with four (#412)

  • NEW: An interrupted download resumes where it stopped, using Range and If-Range with the server’s strong ETag or an old enough Last-Modified, instead of starting over. A response with a content coding is never resumed (#404)

  • NEW: get_data, find_extracts, get_data_by_area and download accept headers (a caller’s User-Agent, for example), timeout (seconds, or a (connect, read) pair) and opener (such as urllib.request.build_opener()). Caller credentials, cookies and Host are not sent to the target of a redirect. download creates a missing target directory instead of raising ValueError (#405)

  • CHANGED: Downloads, Movisda index fetches and size requests retry network errors and HTTP status 408, 425, 429 and 5xx up to three times, waiting 1 s, 2 s or the server’s Retry-After. A failed download raises pyrosm.exceptions.DownloadError, which is also a ValueError and an OSError as before, with url, status and attempts; ExtractDownloadError.errors holds one for each extract tried (#403)

  • CHANGED: get_data_by_area raises pyrosm.exceptions.ExtractNotFoundError, a ValueError subclass, when no extract contains the area. download, get_data_by_area, get_data_by_bbox and geocode report their progress through the pyrosm logger at INFO level instead of printing it (#402)

  • FIXED: An extract whose size Geofabrik does not give (a failed or looping HEAD request) is ranked by the size recorded in pyrosm’s Geofabrik index, with a warning, instead of last; a redirect loop is not retried. scripts/update_extract_indexes.py geofabrik-sizes refreshes the recorded sizes (#406)

  • FIXED: When Movisda’s grid index cannot be fetched and no copy is cached, find_extracts and get_data_by_area read the copy that ships with pyrosm (28,350 tiles), with a warning, instead of leaving out every Movisda extract. When the administrative index cannot be fetched, only the administrative areas are left out (#407)

  • FIXED: A crop or merge with workers > 1 (OSM.to_pbf, merge_pbf, get_data_by_area) no longer hangs when its worker processes cannot start, as on macOS and Windows for a script without an if __name__ == "__main__": guard or one read from stdin; it runs in one process with a RuntimeWarning (#416)

  • DOCS: The get_data_by_area docstring says which features a Movisda grid tile leaves out at its edges. The downloading guide and the docstrings show how to require the streets around transit stops (must_cover with buffered stops or a hull of them), how to see the extract get_data_by_area would download before downloading it, and how to download a large region resumably and crop it with merge_pbf (#414, #415)

v0.14.0 (Sep 30, 2026)#

This release adds two ways to get the data for an area that crosses the borders of the published extracts: get_data_by_area downloads the smallest single extract from Geofabrik, BBBike or Movisda that contains the area, and merge_pbf merges overlapping extracts into one file. get_data can now download every extract Geofabrik publishes.

  • NEW: get_data can download every extract Geofabrik publishes. The catalogue gains the regions of China, India, Indonesia, Australia, the Czech Republic and Norway (listed under pyrosm.data.available["subregions"]), the administrative regions of British Columbia and Nunavut, the London borough enfield, the cross-border extracts alps, britain_and_ireland, dach, guernsey_jersey and sea, and american_oceania, bermuda, falklands, guyana and us_virgin_islands. get_data("beijing") and get_data("victoria") are now ambiguous, as each names both a BBBike city and a Geofabrik region: use "cities/beijing" or "china/beijing", and "cities/victoria" or "australia/victoria". sources.asia.china, and the same attribute of the other countries that gained regions, now returns the country’s region group, as sources.europe.france does; the whole-country file stays available through get_data("china") and the group’s country record (#394)

  • NEW: Add pyrosm.get_data_by_area(area, crop=True, update=False, directory=None, output_path=None) to download the smallest single OSM extract that contains an area and, by default, crop it to the area’s bounding box. It compares Geofabrik extracts, BBBike city extracts and Movisda’s administrative areas and 1°/10° grid tiles by download size and never merges extracts; when a download fails, the next smallest extract is tried. The area may be a polygon, a GeoDataFrame/GeoSeries or a bounding box. It returns an AreaExtract record (the file, provider, extract, URL, size and timings) that OSM() accepts as a path, and raises pyrosm.exceptions.ExtractDownloadError when no extract could be downloaded. The BBBike city list ships with pyrosm; Movisda’s index files are fetched and cached next to the downloads. All pyrosm downloads now send a pyrosm User-Agent and give up on a server that stops responding for 60 seconds, and a download that fails midway no longer leaves a partial file that later calls reuse (#389, #390, #391, #392)

  • NEW: Add pyrosm.merge_pbf(inputs, output_path=None, bounding_box=None, keep_relations=True, workers=1) to merge overlapping .osm.pbf extracts, for example two country extracts that together cover a border city, into one file sorted by type then id. An element found in several inputs is written once (the highest version, then the latest timestamp, then the earliest input), and inputs with and without element metadata can be mixed. With bounding_box the crop rule of OSM.to_pbf is applied to the union of the inputs, and a kept way keeps its full node list whichever input holds the nodes. The inputs are streamed block by block and workers parallelises the crop selection. The output is written to a temporary file and moved into place when complete, and the merge fails if an input’s size or modification time changes, or the file is replaced, while it runs (a sync client such as OneDrive touching only a file’s metadata does not count as a change). to_pbf(repack=True) now writes a file that mixes nodes with and without element metadata instead of raising, and re-packed blocks are split to stay below the 16 MiB the PBF format recommends (#388, #398)

  • FIXED: Cropping a truncated or corrupt PBF with OSM.to_pbf raises an InvalidOSMFileError that names the file. The crop read the data blocks without checking them, so a truncated download failed with a bare protobuf DecodeError, zlib.error or struct.error. The crop and the header check of OSM() now share one blob reader, which also rejects blobs declared or decompressed beyond the 32 MiB PBF limit. The crop’s ValueError for an unsupported compression or required feature, and the PBFNotImplemented of OSM() for an unsupported required feature, name the file too (#385)

  • FIXED: Graph simplification (OSM.to_graph(..., simplify=True)) keeps one edge where two OSM ways overlap node for node along a simplified chain, as OSMnx does. It kept a parallel copy of the stretch for each way, which counted it twice in the edge count and the total length (#386)

  • FIXED: Spain’s Geofabrik regions download by name. They were listed with accents and spaces (“Castilla y León”), which produced URLs Geofabrik does not serve; they now use ASCII names (castilla_y_leon, pais_vasco) and are also available as sources.europe.spain. get_data("madrid") is now ambiguous, as it names both a BBBike city and a Geofabrik region: use "cities/madrid" or "spain/madrid" (#381)

  • FIXED: Russia’s whole-country record (sources.europe.russia.country) points at Geofabrik’s russia-latest.osm.pbf. It pointed at russia/russia-latest.osm.pbf, which Geofabrik does not serve; get_data("russia") was not affected (#395)

Thanks for all the contributors who helped to improve the library either via PRs, or by raising or participating in an issue:

v0.13.1 (Aug 1, 2026)#

A packaging release: the wheels are considerably smaller and Linux ARM wheels are published. The library itself is unchanged.

  • CHANGED: The wheels no longer carry the debug information and the generated C sources. The manylinux images compile with -g and auditwheel does not strip, so the DWARF sections made up 88% of every compiled module, and include_package_data shipped the Cython-generated .c files next to the .pyx sources inside every binary wheel. The Linux wheel drops from 12.0 MB to about 3 MB (~44.7 MB to ~6 MB installed), and the macOS and Windows wheels from about 5.1 MB to 3.1 MB. The source distribution still contains the C sources, so a source build needs no Cython (#374)

  • NEW: Publish Linux ARM (aarch64) wheels, built natively on an ARM runner. Installing on Linux ARM no longer builds pyrosm from source (#374)

v0.13.0 (Jul 29, 2026)#

This release aligns the street-network reading and the networkx graph export with OSMnx: the predefined network_type filters now match the OSMnx way filters one-to-one, and an exported graph works with the OSMnx functions. The other readers are unchanged.

  • CHANGED: The predefined network types match the way filters of OSMnx one-to-one, so the same query returns the same streets in both libraries. driving is OSMnx’s drive and no longer includes service roads (use driving+service, which is OSMnx’s drive_service, to keep them), all is filtered rather than returning every way, and the new all_public leaves out the privately accessible ways. Every mode-specific network now leaves out access=private ways, walking leaves out ways whose sidewalk is mapped separately, and the lifecycle and service-area highway values are excluded throughout. The OSMnx names drive, drive_service, walk and bike are accepted as network types as well (#369, #372)

  • CHANGED: driving no longer excludes ways tagged psv=yes. The tag means public service vehicles are allowed rather than others banned, so the exclusion dropped ordinary drivable streets carrying a bus route. driving+service used to resolve to that filter, which added public service vehicle ways instead of service roads (#369, #372)

  • FIXED: A networkx graph exported with osmnx_compatible=True works with OSMnx. key is no longer an edge attribute, where it collided with the parallel-edge key of add_edge, and osmid is the key of a node rather than also a node attribute, where it collided with the node index when writing the nodes. Node tags are stored as a JSON string like the edge tags, and the graph carries simplified (#370, #371)

  • FIXED: Parallel edges between the same pair of nodes survive the networkx export. Every edge was written with the multi-edge key 0, so all but the last one were dropped from the graph (#370, #371)

  • FIXED: An edges GeoDataFrame with gaps or duplicate labels in its index exports to a graph. The edge attributes were read by index label, and the one-way masks were selected by label, which raised for a filtered or concatenated frame (#370, #371)

  • FIXED: An unknown network_type raises a ValueError in every reader. network_type="driving_psv" was accepted but unhandled, and returned the whole unfiltered network (#369, #372)

  • FIXED: A tag holding several values (access=private;delivery) is matched by the predefined network filters, as the OSMnx filters match it. A custom_filter is unaffected and keeps matching values exactly (#369, #372)

Thanks for all the contributors who helped to improve the library either via PRs, or by raising or participating in an issue:

v0.12.0 (Jul 22, 2026)#

This release makes OSM.write_pbf able to edit geometry – moving nodes, reshaping ways and deleting elements – so a network read with pyrosm can be edited and written back to a *.osm.pbf in full. The reading engines are unchanged.

  • NEW: OSM.write_pbf can edit geometry, not just tags. Pass apply_geometry=True to move an existing node (a node row’s Point sets its coordinates, and every way and relation referencing it follows), to reshape an existing way through its nodes member-id column (inserting, removing and reordering vertices), and to add a new element under a caller-chosen negative id so a way’s nodes list can reference a vertex added in the same call. The new delete parameter takes (osm_type, id) pairs to omit elements from the output, with on_orphan_node ('remove'/'keep'/'error') and on_deleted_member ('drop'/'error') deciding what happens to the references a removal breaks. Defaults are unchanged: without apply_geometry/delete, write_pbf writes exactly what it wrote before (#367)

  • NEW: Add a keep_node_info parameter to OSM(...), which keeps the nodes column of each way’s ordered member node ids in the returned GeoDataFrames. It was previously only reachable by setting the attribute after construction, and it is what write_pbf(..., apply_geometry=True) reshapes a way through (#367)

  • NEW: Add California to the US state sources, so get_data("california") downloads the whole-state Geofabrik extract. The northern_california and southern_california sub-regions are unchanged (#366)

Thanks for all the contributors who helped to improve the library either via PRs, or by raising or participating in an issue:

  • mitchellhenke (#366)

v0.11.0 (Jul 1, 2026)#

This release adds topological graph simplification for network exports, opt-in regular-expression and Overpass-style custom filters, and selective-layer PBF export, and rewrites multipolygon relation assembly to follow the OSM even-odd algorithm so relation geometries match osmium. It also reorganizes the documentation with a GeoPandas-style API reference. The default in-memory and out-of-core readers are otherwise unchanged.

  • NEW: Add topological graph simplification to OSM.to_graph via simplify=True (off by default). It removes interstitial (degree-two) nodes and merges each chain of edges between true intersections and dead-ends into a single edge – consolidating their geometry and length – so the exported graph matches the topology produced by OSMnx’s simplify_graph. Pass simplify_kwargs to forward options, and the kept nodes carry an OSMnx-compatible street_count. It is implemented vectorised with a single Cython chain-walk kernel and needs no networkx/igraph in the path (#89, #351, #352)

  • NEW: custom_filter accepts two opt-in advanced forms in addition to the exact-value dict, on every reading method: a compiled regular expression (re.compile(...)) as a value, matched against the tag value with re.search; and Overpass-style bracket-filter strings – a single string or a list of them, the syntax OSMnx users already know – where each string is the AND of its brackets and a list is their OR, supporting =, !=, ~, !~, key-present and key-absent operators and a case-insensitive flag. Advanced network filters select ways by the keys they reference, so get_network can now build non-highway networks (e.g. railway or cycleway). A plain-dict filter behaves exactly as before (#116, #341, #357, #359)

  • NEW: Add a subset_only parameter to OSM.write_pbf for exporting only selected layers to a PBF. With subset_only=True the writer keeps only the elements in the supplied GeoDataFrame(s) and the nodes/ways they reference, rather than the whole source file, so e.g. just the buildings and the network can be written to a new *.osm.pbf (#348, #353)

  • FIXED: Assemble multipolygon and boundary relations with the OSM even-odd rule – a point is inside the area when an odd number of the relation’s rings enclose it – instead of the previous role-based outer/inner classification. This matches osmium and GDAL and corrects relation geometries with islands-in-holes, touching inner rings, or mis-tagged member roles, and drops spurious force-closed artifacts. It also fixes the out-of-core engine returning fewer elements under keep_other_tags=False, which had omitted the relation linestring-decision tag keys (#21, #354, #355)

  • DOCS: Reorganize the documentation with a grouped table of contents and a new GeoPandas-style API reference (category tables with a one-line summary per method and a page per object); add a Frequently Asked Questions page (including how to find street intersections and their coordinates); document graph simplification and the advanced custom filters; render the API reference correctly on Read the Docs; and fix broken documentation links in the README (#93, #177, #347, #349, #350, #356, #358, #360)

  • TESTS: Add a parity test suite cross-validating pyrosm against OSMnx on identical data, area and time – OSMnx reference layers are fetched from the Overpass API at the PBF’s historical moment (an Overpass attic query) and compared against pyrosm’s read of the same bounding box and filter, asserting at least 0.995 Jaccard similarity on the selected OSM ids for Overpass-style network filters and building filters (#362)

Thanks for all the contributors who helped to improve the library either via PRs, or by raising or participating in an issue:

v0.10.0 (Jun 26, 2026)#

This release adds an opt-in out-of-core (“streaming”) reading engine that decodes large PBF files in a single streaming pass with bounded memory – the decode, the node-coordinate gather and the standalone-way read all run in parallel across a worker pool, and each layer’s result is cached automatically – making whole-country and even whole-continent extracts readable quickly on modest machines without running out of memory (tested on Macbook Air with 24 GB of RAM). get_data can now also download whole-continent extracts. The default in-memory reader is unchanged.

  • NEW: Add an opt-in out-of-core reading engine, selected with OSM(filepath, engine="out_of_core") (the default engine="in_memory" is unchanged). It decodes the PBF in a single streaming pass – each block’s node coordinates and matching features are spilled to per-block files on disk, then only the coordinates the kept features reference are gathered to assemble geometries – so peak memory is bounded by the working set rather than the whole file, making whole-country extracts readable on modest machines, resolving the out-of-memory errors and kernel crashes previously reported when reading large extracts (#111, #147, #166, #205). Every feature method (get_network, get_buildings, get_pois, get_landuse, get_natural, get_boundaries, get_data_by_custom_criteria) and the options custom_filter, bounding_box, complete_relations, extra_attributes/tags_to_keep and keep_metadata behave as on the in-memory reader and return column-for-column identical GeoDataFrames; get_network(nodes=True) returns the graph-export nodes and edges. History (.osh.pbf) files and timestamped reads are served by the in-memory reader (#321, #322, #323, #324, #325, #327, #329)

  • NEW: The out-of-core engine caches each layer’s result to a GeoParquet file under a temporary directory, keyed by the source file and the read parameters, and reuses it on identical later reads – even in a later Python session – instead of re-decoding the PBF. Caching uses the optional pyarrow package; without it the reader still works but returns an in-memory GeoDataFrame without caching. Passing output="path" streams a layer straight to a GeoParquet file instead (#330, #331)

  • NEW: Control out-of-core parallelism with a workers parameter on OSM(...). By default the engine reads on a single core and the first out-of-core read reports how many CPU cores are available; pass workers="auto" to let pyrosm pick the count by file size (one worker per CPU core above ~70 MB, capped at the data-blob count), or workers=N for an explicit count (reduced to the CPU-core count, with a warning, if it exceeds it). On macOS and Windows a parallel read launched from a standalone script must run under an if __name__ == "__main__": guard; without it the read falls back to a single process with a warning (#326, #328, #334)

  • NEW: Add OSM helper methods for managing the out-of-core engine’s temporary files. OSM.list_cache() and OSM.clear_cache() list and remove the cached layer GeoParquet files, and OSM.list_downloads() and OSM.clear_downloads() list and remove the *.osm.pbf extracts get_data downloads to the temp directory. list_cache/clear_cache and clear_downloads take an optional source file to scope the operation to a single file; the bundled package datasets are never touched (#336)

  • NEW: Add a keep_other_tags parameter (default True) to the out-of-core get_data_by_custom_criteria. With keep_other_tags=False the engine resolves only the requested tags (tags_as_columns plus the filter keys) and returns no JSON tags column of leftover tags, so a geometry-and-key read skips the long-tail tag parsing (and the leftover column that would have been dropped is no longer built at all). Only the out-of-core engine supports it – the in-memory reader raises (#337, #340)

  • NEW: get_data now downloads whole-continent extracts – get_data("south_america") (and the other continents) resolves to the continent-wide Geofabrik *-latest.osm.pbf instead of raising, so an entire continent can be fetched in one call and then read with the out-of-core engine’s bounded memory (#339)

  • NEW: Add Spain’s autonomous communities as Geofabrik sub-region sources, so each can be downloaded individually with get_data (#203)

  • CHANGED: Parallelise the out-of-core engine’s collect phase. After the parallel decode, the node-coordinate gather and the standalone-way read are split across the same worker pool (the way records are carried as columnar arrays so they cross process boundaries cheaply) instead of running single-process. On country-scale reads this collect phase is the dominant post-decode cost, so parallelising it markedly speeds up large reads, with byte-identical output (#338)

  • CHANGED: Spill the out-of-core engine’s intermediate shards coarser – decode now batches several PrimitiveBlocks into one shard (up to a byte target) rather than writing one shard per ~8k-element block, so a country file produces a few hundred shards instead of tens of thousands of tiny files. This cuts the per-file open/read overhead the collect phase pays re-reading them (roughly halving the collect time on a Finland buildings read), with no change to the output, and keeps peak memory bounded by about one shard per worker (#342)

Thanks for all the contributors who helped to improve the library either via PRs, or by raising or participating in an issue:

v0.9.1 (Jun 18, 2026)#

This release adds reading all tagged data without a filter and correct relation geometries for bounding-box reads, speeds up area-feature (Polygon) geometry building, and fixes incomplete-boundary handling and ambiguous region-name lookups.

  • NEW: Add a complete_relations parameter to OSM(...) (default False). When reading with a bounding_box, a relation (e.g. a multipolygon or boundary) is assembled from only the member ways inside the box, so a relation straddling the box edge gets a partial geometry. With complete_relations=True the reader fetches each such relation’s full member set (member ways and their nodes, even outside the box) so the geometry is correct, applying to every relation-returning layer (get_buildings, get_landuse, get_natural, get_boundaries, get_pois, get_data_by_custom_criteria). It is opt-in: the default reproduces the existing output, and completion adds two streaming passes over the file only when a relation actually has missing members. The fetched member ways are kept out of the normal way features, so other layers (e.g. get_network) are unaffected. Only member ways are completed; relations whose members are themselves relations (super-relations) are not. A whole-file read (no bounding_box) already holds every member, so the option is a no-op there. When a bounding-box read returns relations the box cut and complete_relations was not enabled, a UserWarning reports how many relations were returned with incomplete geometry and points to the option (#236)

  • NEW: get_data_by_custom_criteria now accepts custom_filter=None (the new default) to read every tagged element without enumerating tag keys – tagged nodes as Points, ways as Lines/Polygons and relations as (Multi)Polygons/Lines; standalone untagged ways are dropped (matching GDAL’s OSM driver). When tags_as_columns is not given it defaults to the union of the per-feature default tag columns, so common keys become columns and the rest land in the JSON tags column, and keep_metadata/keep_nodes/keep_ways/keep_relations still apply (#113)

  • FIXED: get_boundaries() no longer force-closes incomplete administrative boundaries into polygons. A boundary relation whose member ways run off the PBF extent cannot form a closed ring, and was previously closed with a spurious straight edge bridging the gap (the stray lines crossing boundary plots); such incomplete boundaries are now dropped, matching how osmium and GDAL skip areas they cannot assemble (#154)

  • FIXED: get_data() no longer silently returns the wrong extract when a dataset name is shared by multiple regions – e.g. get_data("georgia") (the US state vs. the country) now raises a ValueError listing the region-qualified alternatives instead of returning the first match. Region-qualified names are accepted (get_data("usa/georgia") vs. get_data("europe/georgia")), and names that resolve to the same file (e.g. a UK county reachable via both great_britain and united_kingdom) still resolve as before (#162)

  • CHANGED: Vectorise closed-area (Polygon) way geometry construction – build the geometries for closed area ways (e.g. get_buildings, get_landuse, get_natural) with a single batched shapely.polygons/shapely.linearrings call instead of a per-way Python loop, falling back to the exact per-way builder for the rest (open ways, closed ways tagged as linear features, ways with dropped nodes), so the output is identical. Cuts get_buildings wall-clock ~28% on an Estonia extract (52.0 s -> 37.4 s), complementing the network-geometry vectorisation added in v0.9.0 (#315)

Thanks for all the contributors who helped to improve the library either via PRs or reporting bugs:

v0.9.0 (Jun 16, 2026)#

This release lowers the read path’s memory use and speeds it up, adds fetching data by bounding box and by place name, and writes OSM data back to PBF (editing attributes/tags and cropping).

  • NEW: Add a tags_to_keep parameter to the feature methods (get_network, get_buildings, get_pois, get_landuse, get_natural, get_boundaries). When given, only those OSM tag keys are kept as columns (replacing the default tag-column set), reducing memory; structural columns, filtering and extra_attributes are unaffected and the default behaviour is unchanged (#87)

  • NEW: Add a keep_metadata parameter to OSM(...) (default True). Set keep_metadata=False to drop the element metadata columns (timestamp, version, changeset) from the returned GeoDataFrames and skip decoding the per-node metadata while parsing, lowering memory use and parse time on node-heavy files; the default behaviour is unchanged and history (.osh.pbf) files keep the metadata they require (#87, #150)

  • CHANGED: Replace the per-node coordinate lookup (previously a dict-of-dicts) with a compact cykhash id->index map plus contiguous column arrays, cutting the read path’s peak memory (~12% on a 138 MB extract) and making coordinate lookups during geometry building a little faster, with no change to the returned data (#53)

  • CHANGED: Build network way geometries with a single batched shapely.linestrings call across ways (and skip the from/to ids and node-attribute records that a plain get_network discards), cutting network geometry construction ~37% and the multi-layer wall-clock ~17% on a 138 MB extract, with no change to the returned data (#53)

  • CHANGED: Stream PBF blocks through a generator (each block is parsed then discarded) instead of holding the whole decompressed file in a list, cutting the read path’s peak memory – ~20% for a whole-file read and ~50-70% for bounding-box reads, which no longer pay the whole-file cost. Output is unchanged; bounding-box reads re-stream the file once to complete boundary geometries, which adds roughly 10% time to those reads (#53)

  • CHANGED: When splitting node tags into columns, build only the tag-columns that actually occur in the data instead of materialising every candidate column and dropping the all-empty ones, cutting get_pois (and other node-feature) assembly ~37% and the multi-layer wall-clock ~14% on a 138 MB extract, with no change to the returned data (#53)

  • CHANGED: Apply the same build-only-occurring-columns approach when splitting way and relation tags into columns (get_network, get_buildings, get_landuse, get_natural and custom-criteria reads), so candidate tag-columns absent from the data are no longer materialised and dropped, cutting the way/relation tag-assembly step ~12-16% on a 138 MB extract (the full feature call improves less, as geometry construction dominates these features), with no change to the returned data (#53)

  • NEW: Add OSM.write_pbf(data, output_path) to write the OSM data back to a valid, re-readable PBF after modifying attributes/tags in pandas (e.g. fill maxspeed, add a travel_time tag). The whole dataset that was read is written; each row of data (a GeoDataFrame or list of them) updates the tags of the matching element by osm_type``+``id, and rows whose id is not in the source are added as new elements synthesized from their geometry (Point->node, LineString->way, hole-less Polygon->closed way; negative ids). Topology and coordinates come from the parsed data, so the output round-trips coordinates exactly and is read by pyrosm, osmium, GDAL and r5py/R5 (a modified pedestrian/car network exported from pyrosm builds a routable R5 network). v1 applies edits and additions, not deletions (#286, #285)

  • NEW: Add OSM.to_pbf(output_path=None, keep_relations=True, workers=1, compact=False, repack=False) to crop the source .osm.pbf by the object’s bounding_box and write a valid, re-readable PBF to disk (a temp file when no path is given). Cropping is memory-efficient (streams the file blob-by-blob, holds only id sets) and “complete-ways” (a way is kept when >=1 node is inside the box and keeps its full node list); coordinates round-trip exactly. workers>1 parallelizes the per-block work over a single process pool and produces byte-identical output (with a sequential fallback for files too small to amortize pool startup). compact=True prunes each output block’s string table to only the strings its kept elements reference, producing a smaller file (~18% smaller on a Helsinki-region crop) at the cost of some extra per-block work. repack=True goes further and re-chunks the kept elements into canonical, densely packed blocks (as osmium/Osmosis produce) for the smallest output, at the cost of speed (the re-pack write is sequential, though workers still parallelizes the selection); it supersedes compact. The defaults compact=False/repack=False keep the faster current behaviour, and the written OSM data – coordinates, tags and element metadata – is identical for every combination (#284, #6)

  • CHANGED: Import pyrosm lazily so import pyrosm no longer eagerly loads geopandas/shapely (~2 s); OSM, get_data and get_path are still importable as before and load on first use (#284, #6)

  • NEW: Add pyrosm.get_data_by_bbox(bbox, crop=True, download=True, update=False, directory=None, output_path=None) to download – and by default crop – the OSM data covering a bounding box. It finds the smallest Geofabrik extract whose extent fully covers the box, downloads it, and (with crop=True, the default) crops it to the box, returning the cropped file named bbox_<minx>_<miny>_<maxx>_<maxy>.osm.pbf; crop=False returns the full extract, and download=False returns the covering extract’s PBF URL without downloading. The bounding box may be a [minx, miny, maxx, maxy] list/tuple/array in lon/lat, a Shapely geometry, or a GeoDataFrame/GeoSeries. It is backed by a vendored snapshot of Geofabrik’s index-v1.json, so the extract lookup works offline (refresh it with scripts/update_geofabrik_index.py); update=True refreshes the index and re-downloads (#165, #197)

  • NEW: Add pyrosm.geocode(query) and pyrosm.get_data_by_geocoding(query, crop=True, download=True, update=False, directory=None, output_path=None) to fetch data by place name. geocode returns a Shapely polygon for a place (e.g. "Brighton and Hove, UK") via OpenStreetMap’s Nominatim service – its boundary polygon when available, otherwise its bounding-box rectangle. get_data_by_geocoding geocodes the place, then downloads – and by default crops – the smallest Geofabrik extract that covers it, returning the cropped file named after the place (e.g. brighton-and-hove-uk.osm.pbf); crop=False returns the full extract and download=False returns the covering extract’s PBF URL. No new dependencies (stdlib urllib/json with the bundled certifi, and shapely) (#165)

Thanks for all the contributors who helped to improve the library either via PRs or reporting bugs:

v0.8.0 (Jun 10, 2026)#

This is a major release that changes the PBF parsing backend.

  • CHANGED: Replace the Pyrobuf PBF backend with Google’s Protobuf (its fast C upb backend) for parsing the protocol-buffer messages. Pyrobuf is unmaintained and its source build fails with modern setuptools (breaking pip install pyrosm); Google’s Protobuf is actively maintained and ships wheels and conda-forge packages for Python 3.10–3.14. Parsing speed is unchanged — see the backend benchmark. v0.8.0 is the first release built on Google’s Protobuf; v0.7.0 was the last to use Pyrobuf. (#276)

  • NEW: Automate PyPI releases — a GitHub Actions release workflow builds binary wheels (cibuildwheel; Linux/macOS/Windows × CPython 3.10–3.14) and an sdist, then publishes them to PyPI via Trusted Publishing and creates a GitHub release when a vX.Y.Z tag is pushed (#288, #287)

  • NEW: Expose relation members under the members key of the JSON tags column (each {member_id, member_type, member_role}), so relations carry their members in the returned GeoDataFrame (#281, #216)

  • NEW: Raise a clear InvalidOSMFileError when the input .pbf is not a valid OSM PBF file, instead of a cryptic zlib/protobuf error (#280, #160)

  • NEW: Accept pathlib.Path (and any os.PathLike) filepaths in the OSM constructor, not just strings (#279, #145)

  • FIXED: Decode node coordinates at full float64 precision (exact OSM 7-decimal values, matching GDAL/osmium); they were truncated to float32, introducing a ~0.1 m error, false extra precision, and visible distortion of straight geometry edges (#283, #245, #225)

  • FIXED: Normalize polygon/multipolygon ring orientation to the OGC/GeoJSON right-hand rule (exterior counter-clockwise, holes clockwise), matching osmium and QGIS; previously rings inherited the OSM way node order and were inconsistently wound (#282, #230)

  • FIXED: get_bounding_box now reads the header bounding box correctly; it returned None for every file after the protobuf backend migration (#280, #160)

  • FIXED: Download data over HTTPS using certifi’s CA bundle instead of the OS trust store, so fetching datasets no longer fails on Windows with ssl.SSLError [ASN1: NOT_ENOUGH_DATA] (a CPython bug triggered by a malformed entry in the Windows certificate store) (#294)

  • Refresh the README badges (#278)

Thanks for all the contributors who helped to improve the library either via PRs or reporting bugs:

v0.7.0 (Jun 7, 2026)#

  • NEW: Add pandarm graph-export backend (the maintained, NumPy 2-compatible fork of pandana); deprecate graph_type="pandana" (#271)

  • NEW: Make cycling networks directed and honour oneway:bicycle (#255)

  • NEW: Add custom_filter to get_network so custom-filtered networks also return graph nodes (#264)

  • NEW: Add street_count node attribute to the NetworkX export (compatible with OSMnx basic_stats) (#265)

  • NEW: Support combining custom_filter True with explicit tag values (#251)

  • Support Python 3.10–3.14 (drop 3.9) and fix OSH parsing under pandas 3.0 (#248)

  • Return complete (uncut) geometries for ways/edges that straddle a bounding-box edge (#268)

  • Keep bounding-box network nodes consistent with the kept edges so graph export works without manual cleanup (#269)

  • Fix non-dense PBF node parsing (parse_nodes) (#275)

  • Handle bounding boxes that select no nodes instead of raising KeyError (#267)

  • Fix custom_filter with highway turning closed-way polygons into lines (#266)

  • Fix network exclude/keep filters leaking on multi-key filters (#263)

  • Fix duplicate “phantom” nodes in the NetworkX export (#259)

  • Correct relation ids and surface a colliding id tag as id_tag (#234, #249)

  • Stop get_* methods from mutating the shared default-tag config (#252)

  • Fix spurious pandas chained-assignment warnings from the Cython frame builders (#256)

  • Fix Geofabrik UK sub-region downloads (moved under united-kingdom) (#258)

  • Fix reading PBF produced by osmconvert (#238)

  • Fix documentation URL (#223)

  • Measure Cython (.pyx) coverage and raise overall test coverage (#273)

  • Document the pandarm graph backend and the pandana deprecation, and reading OSM history files (.osh.pbf) (#257)

  • Fix the Read the Docs build; run live download tests on a single CI runner; bump GitHub Actions to Node 24 (#250, #254, #260)

Thanks for all the contributors who helped to improve the library either via PRs or reporting bugs:

v0.6.2 (Oct 26, 2023)#

  • Fix installation issues and support only Python >= 3.9 (#221)

  • Fix GA actions and use micromamba to install environments (#221)

  • Use Shapely 2.0 instead of pygeos (#214)

Thanks for the following contributors:

v0.6.1 (Oct 11, 2021)#

  • Support Python 3.9 (#122, #106)

  • Use Github Actions for CI (#95)

  • Drop Travis CI (#95)

  • Add contribution guidelines (#90)

  • Follow PEP8 style guide and add linting test for CI using Black

v0.6.0 (Nov 18, 2020)#

  • NEW: Adds possibility to export street networks to igraph, networkx and pandana (#57, #58, #70) - Add functionality to parse/return the nodes of the network when requested (#52) - Calculate length of the edge for networks in meters (#56, #70) - Filter out weakly connected component by default when exporting to graph (#59) - Add (vectorized) functionality to create directed edges according oneway rules (#68)

  • Fix installation issue with pip on Windows (#61)

  • Fix numpy deprecation warning (#50)

  • Update the documentation to use new theme (#74)

  • Add possibility to test the tool using JupyterLab in browser (#75)

  • Fix issue when parsing POIs using rare tags as a custom filter (#47)

  • Fix issue when filtering with bounding box polygon (#54)

  • Add documentation about exporting the networks to graphs (#69)

  • Improve documentation overall

v0.5.3 (Sep 13, 2020)#

Changes:

  • Ensures that geometry construction works with new Pygeos release v0.8.0 (#46)

v0.5.1/2 (May 11, 2020)#

  • Fix multi-level filtering

  • Add support for using “exclude” also with nodes and relations

  • Fix data source for New York City

v0.5.0 (May 7, 2020)#

  • Adds a function to download PBF data from Geofabrik and BBBike easily from hundreds of locations across the world

  • Improved geometry parsing for relations

  • Parse boundary geometries as Polygons instead of LinearRings (following OSM definition)

  • Fix invalid geometries automatically (self-intersection and “bowties”)

  • Add better documentation about custom filters

  • Make parsing more robust for incorrectly tagged OSM entries.

  • Bug fixes

  • Update website to a new theme.

v0.4.3 (April 27, 2020)#

  • Fixes a bug related to filtering with custom filters (see details here.)

v0.4.2 (April 23, 2020)#

  • Add functionality to parse boundaries from PBF (+ integrate name search for finding e.g. specific administrative boundary)

  • Support using Shapely Polygon / MultiPolygon to filter the data spatially

  • add possibility to add “extra attributes” (i.e. OSM keys) that will be parsed as columns.

  • improve documentation

v0.4.1 (April 17, 2020)#

v0.4.0 (April 16, 2020)#

  • read PBF using custom queries (allows anything to be fetched)

  • read landuse from PBF

  • read natural from PBF

  • improve geometry parsing so that geometry type is read automatically according OSM rules

  • modularize code-base

  • improve test coverage

v0.3.1 (April 15, 2020)#

  • generalize code base

  • read Points of Interest (POI) from PBF

v0.2.0 (April 13, 2020)#

  • read buildings from PBF into GeoDataFrame

  • enable applying custom filter to filter data: e.g. with buildings you can filter specific types of buildings with {‘building’: [‘residential’, ‘retail’]}

  • handle Relations as well

  • handle cases where data is not available (warn user and return empty GeoDataFrame)

v0.1.8 (April 8, 2020)#

  • read street networks from PBF into GeoDataFrame (separately for driving, cycling, walking and all-combined)

  • filter data based on bounding box

v0.1.0 (April 7, 2020)#

  • first release on PyPi