QuackOSM 0.17.0
An open-source tool for reading OpenStreetMap PBF files using DuckDB
License: Apache-2.0
Requires Python: >=3.10

<p align="center">
  <img width="300" src="https://raw.githubusercontent.com/kraina-ai/quackosm/main/docs/assets/logos/quackosm_logo.png"><br/>
  <small>Generated using DALL·E 3 model with this prompt: A logo for a python library with White background, high quality, 8k. Cute duck and globe with cartography elements. Library for reading OpenStreetMap data using DuckDB.</small>
</p>

<p align="center">
    <img alt="GitHub" src="https://img.shields.io/github/license/kraina-ai/quackosm?logo=apache&logoColor=%23fff">
    <img src="https://img.shields.io/github/checks-status/kraina-ai/quackosm/main?logo=GitHubActions&logoColor=%23fff" alt="Checks">
    <a href="https://github.com/kraina-ai/quackosm/actions/workflows/ci-dev.yml" target="_blank"><img alt="GitHub Workflow Status - DEV" src="https://img.shields.io/github/actions/workflow/status/kraina-ai/quackosm/ci-dev.yml?label=build-dev&logo=GitHubActions&logoColor=%23fff"></a>
    <a href="https://github.com/kraina-ai/quackosm/actions/workflows/ci-prod.yml" target="_blank"><img alt="GitHub Workflow Status - PROD" src="https://img.shields.io/github/actions/workflow/status/kraina-ai/quackosm/ci-prod.yml?label=build-prod&logo=GitHubActions&logoColor=%23fff"></a>
    <a href="https://results.pre-commit.ci/latest/github/kraina-ai/quackosm/main" target="_blank"><img src="https://results.pre-commit.ci/badge/github/kraina-ai/quackosm/main.svg" alt="pre-commit.ci status"></a>
    <a href="https://www.codefactor.io/repository/github/kraina-ai/quackosm"><img alt="CodeFactor Grade" src="https://img.shields.io/codefactor/grade/github/kraina-ai/quackosm?logo=codefactor&logoColor=%23fff"></a>
    <a href="https://app.codecov.io/gh/kraina-ai/quackosm/tree/main"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/kraina-ai/quackosm?logo=codecov&token=PRS4E02ZX0&logoColor=%23fff"></a>
    <a href="https://pypi.org/project/quackosm" target="_blank"><img src="https://img.shields.io/pypi/v/quackosm?label=pypi%20package&logo=pypi&logoColor=%23fff" alt="Package version"></a>
    <a href="https://anaconda.org/conda-forge/quackosm" target="_blank"><img src="https://img.shields.io/conda/vn/conda-forge/quackosm?&logo=anaconda&logoColor=%23fff" alt="Package version"></a>
    <a href="https://pypi.org/project/quackosm" target="_blank"><img src="https://img.shields.io/pypi/pyversions/quackosm.svg?logo=python&logoColor=%23fff" alt="Supported Python versions"></a>
    <a href="https://pypi.org/project/quackosm" target="_blank"><img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dm/quackosm"></a>
</p>

# QuackOSM

An open-source tool for reading OpenStreetMap PBF files using DuckDB.

## What is **QuackOSM** 🦆?

- Scalable reader for OpenStreetMap ProtoBuffer (`pbf`) files.
- Is based on top of `DuckDB`[^1] with its `Spatial`[^2] extension.
- Saves files in the `GeoParquet`[^3] file format for easier integration with modern cloud stacks.
- Utilizes multithreading unlike GDAL that works in a single thread only.
- Can filter data based on geometry without the need for `ogr2ogr` clipping before operation.
- Can filter data based on OSM tags (with negations and wildcards).
- Can automatically download required PBF files for a given geometry.
- Utilizes caching to reduce repeatable computations.
- Can be used as Python module as well as a beautiful CLI based on `Typer`[^4].

[^1]: [DuckDB Website](https://duckdb.org/)
[^2]: [DuckDB Spatial extension repository](https://github.com/duckdb/duckdb_spatial)
[^3]: [GeoParquet data format](https://geoparquet.org/)
[^4]: [Typer docs](https://typer.tiangolo.com/)

## Installing

### As pure Python module

```bash
pip install quackosm
# or
uv pip install quackosm
```

### With beautiful CLI

```bash
pip install quackosm[cli]
# or
uv pip install quackosm[cli]
```

### From conda-forge

```bash
# Automatically installs with CLI
conda install conda-forge::quackosm
```

### Required Python version?

QuackOSM supports **Python >= 3.10**

### Dependencies

Required:

- `duckdb (>=1.1.2)`: For all DuckDB operations on PBF files and sorting result file (with `spatial` extension)

- `pyarrow (>=16.0.0)`: For parquet files wrangling

- `geoarrow-pyarrow (>=0.1.2)`: For GeoParquet IO operations and transforming Arrow data to Shapely objects

- `geopandas (>=0.6)`: For returning GeoDataFrames and reading Geo files

- `shapely (>=2.0)`: For parsing WKT and GeoJSON strings and fixing geometries

- `typeguard (>=3.0)`: For internal validation of types

- `psutil (>=5.6.2)`: For automatic scaling of parameters based on available resources

- `pooch (>=1.6.0)`: For downloading `*.osm.pbf` files and precalculated OSM indexes

- `rich (>=12.0.0)` & `tqdm (>=4.42.0)`: For showing progress bars

- `requests`: For iterating OSM PBF files services

- `beautifulsoup4`: For parsing HTML files and scraping required information

- `geopy (>=2.0.0)`: For geocoding of strings

Optional:

- `typer[all] (>=0.9.0)` (click, colorama, rich, shellingham): Required in CLI

- `h3` extension for `duckdb`: For transforming H3 indexes into geometries. Required in CLI

- `s2sphere (>=0.2.5)`: For transforming S2 indexes into geometries. Required in CLI

## Usage

### If you already have downloaded the PBF file 📁🗺️

#### Load data as a GeoDataFrame

```python
>>> import quackosm as qosm
>>> qosm.convert_pbf_to_geodataframe(monaco_pbf_path)
                                              tags                      geometry
feature_id
node/10005045289                {'shop': 'bakery'}      POINT (7.42245 43.73105)
node/10020887517  {'leisure': 'swimming_pool', ...      POINT (7.41316 43.73384)
node/10021298117  {'leisure': 'swimming_pool', ...      POINT (7.42777 43.74277)
node/10021298717  {'leisure': 'swimming_pool', ...      POINT (7.42630 43.74097)
node/10025656383  {'ferry': 'yes', 'name': 'Qua...      POINT (7.42550 43.73690)
...                                            ...                           ...
way/990669427     {'amenity': 'shelter', 'shelt...  POLYGON ((7.41461 43.7338...
way/990669428     {'highway': 'secondary', 'jun...  LINESTRING (7.41366 43.73...
way/990669429     {'highway': 'secondary', 'jun...  LINESTRING (7.41376 43.73...
way/990848785     {'addr:city': 'Monaco', 'addr...  POLYGON ((7.41426 43.7339...
way/993121275      {'building': 'yes', 'name': ...  POLYGON ((7.43214 43.7481...

[7906 rows x 2 columns]
```

#### Just convert PBF to GeoParquet

```python
>>> import quackosm as qosm
>>> gpq_path = qosm.convert_pbf_to_parquet(monaco_pbf_path)
>>> gpq_path.as_posix()
'files/monaco_nofilter_noclip_compact.parquet'
```

#### Inspect the file with duckdb

```python
>>> import duckdb
>>> duckdb.load_extension('spatial')
>>> duckdb.read_parquet(str(gpq_path)).order("feature_id")
┌──────────────────┬──────────────────────┬──────────────────────────────────────────────┐
│    feature_id    │         tags         │                   geometry                   │
│     varchar      │ map(varchar, varch…  │                   geometry                   │
├──────────────────┼──────────────────────┼──────────────────────────────────────────────┤
│ node/10005045289 │ {shop=bakery}        │ POINT (7.4224498 43.7310532)                 │
│ node/10020887517 │ {leisure=swimming_…  │ POINT (7.4131561 43.7338391)                 │
│ node/10021298117 │ {leisure=swimming_…  │ POINT (7.4277743 43.7427669)                 │
│ node/10021298717 │ {leisure=swimming_…  │ POINT (7.4263029 43.7409734)                 │
│ node/10025656383 │ {ferry=yes, name=Q…  │ POINT (7.4254971 43.7369002)                 │
│ node/10025656390 │ {amenity=restauran…  │ POINT (7.4269287 43.7368818)                 │
│ node/10025656391 │ {name=Capitainerie…  │ POINT (7.4272127 43.7359593)                 │
│ node/10025656392 │ {name=Direction de…  │ POINT (7.4270392 43.7365262)                 │
│ node/10025656393 │ {name=IQOS, openin…  │ POINT (7.4275175 43.7373195)                 │
│ node/10025656394 │ {artist_name=Anna …  │ POINT (7.4293446 43.737448)                  │
│       ·          │          ·           │              ·                               │
│       ·          │          ·           │              ·                               │
│       ·          │          ·           │              ·                               │
│ way/986864693    │ {natural=bare_rock}  │ POLYGON ((7.4340482 43.745598, 7.4340263 4…  │
│ way/986864694    │ {barrier=wall}       │ LINESTRING (7.4327547 43.7445382, 7.432808…  │
│ way/986864695    │ {natural=bare_rock}  │ POLYGON ((7.4332994 43.7449315, 7.4332912 …  │
│ way/986864696    │ {barrier=wall}       │ LINESTRING (7.4356006 43.7464325, 7.435574…  │
│ way/986864697    │ {natural=bare_rock}  │ POLYGON ((7.4362767 43.74697, 7.4362983 43…  │
│ way/990669427    │ {amenity=shelter, …  │ POLYGON ((7.4146087 43.733883, 7.4146192 4…  │
│ way/990669428    │ {highway=secondary…  │ LINESTRING (7.4136598 43.7334433, 7.413640…  │
│ way/990669429    │ {highway=secondary…  │ LINESTRING (7.4137621 43.7334251, 7.413746…  │
│ way/990848785    │ {addr:city=Monaco,…  │ POLYGON ((7.4142551 43.7339622, 7.4143113 …  │
│ way/993121275    │ {building=yes, nam…  │ POLYGON ((7.4321416 43.7481309, 7.4321638 …  │
├──────────────────┴──────────────────────┴──────────────────────────────────────────────┤
│ 7906 rows (20 shown)                                                         3 columns │
└────────────────────────────────────────────────────────────────────────────────────────┘
```

#### Use as CLI

```console
$ quackosm monaco.osm.pbf
⠋ [   1/32] Reading nodes • 0:00:00
⠋ [   2/32] Filtering nodes - intersection • 0:00:00
⠋ [   3/32] Filtering nodes - tags • 0:00:00
⠋ [   4/32] Calculating distinct filtered nodes ids • 0:00:00
⠋ [   5/32] Reading ways • 0:00:00
⠋ [   6/32] Unnesting ways • 0:00:00
⠋ [   7/32] Filtering ways - valid refs • 0:00:00
⠋ [   8/32] Filtering ways - intersection • 0:00:00
⠋ [   9/32] Filtering ways - tags • 0:00:00
⠋ [  10/32] Calculating distinct filtered ways ids • 0:00:00
⠋ [  11/32] Reading relations • 0:00:00
⠋ [  12/32] Unnesting relations • 0:00:00
⠸ [  13/32] Filtering relations - valid refs • 0:00:00
⠋ [  14/32] Filtering relations - intersection • 0:00:00
⠋ [  15/32] Filtering relations - tags • 0:00:00
⠋ [  16/32] Calculating distinct filtered relations ids • 0:00:00
⠋ [  17/32] Loading required ways - by relations • 0:00:00
⠋ [  18/32] Calculating distinct required ways ids • 0:00:00
⠋ [  19/32] Saving filtered nodes with geometries • 0:00:00
⠋ [20.1/32] Grouping filtered ways - assigning groups • 0:00:00
⠧ [20.2/32] Grouping filtered ways - joining with nodes • 0:00:00
⠋ [20.3/32] Grouping filtered ways - partitioning by group • 0:00:00
  [  21/32] Saving filtered ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:00 < 0:00:00 •
⠋ [22.1/32] Grouping required ways - assigning groups • 0:00:00
⠧ [22.2/32] Grouping required ways - joining with nodes • 0:00:00
⠋ [22.3/32] Grouping required ways - partitioning by group • 0:00:00
  [  23/32] Saving required ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:00 < 0:00:00 •
⠙ [  24/32] Saving filtered ways with geometries • 0:00:00
⠋ [  25/32] Saving valid relations parts • 0:00:00
⠋ [  26/32] Saving relations inner parts • 0:00:00
⠋ [  27/32] Saving relations outer parts • 0:00:00
⠋ [  28/32] Saving relations outer parts with holes • 0:00:00
⠋ [  29/32] Saving relations outer parts without holes • 0:00:00
⠋ [  30/32] Saving filtered relations with geometries • 0:00:00
⠋ [  31/32] Saving all features • 0:00:00
⠋ [  32/32] Saving final geoparquet file • 0:00:00
Finished operation in 0:00:03
files/monaco_nofilter_noclip_compact.parquet
```

### Let the QuackOSM automatically download the required OSM PBF files for you based on geometry 🔎🌍

#### Load data as a GeoDataFrame

```python
>>> import quackosm as qosm
>>> geometry = qosm.geocode_to_geometry("Vatican City")
>>> qosm.convert_geometry_to_geodataframe(geometry)
                                              tags                      geometry
feature_id
node/10253371713   {'crossing': 'uncontrolled',...     POINT (12.45603 41.90454)
node/10253371714               {'highway': 'stop'}     POINT (12.45705 41.90400)
node/10253371715               {'highway': 'stop'}     POINT (12.45242 41.90164)
node/10253371720     {'artwork_type': 'statue',...     POINT (12.45147 41.90484)
node/10253371738               {'natural': 'tree'}     POINT (12.45595 41.90609)
...                                            ...                           ...
way/983015528     {'barrier': 'hedge', 'height'...  POLYGON ((12.45027 41.901...
way/983015529     {'barrier': 'hedge', 'height'...  POLYGON ((12.45028 41.901...
way/983015530     {'barrier': 'hedge', 'height'...  POLYGON ((12.45023 41.901...
way/998561138     {'barrier': 'bollard', 'bicyc...  LINESTRING (12.45821 41.9...
way/998561139     {'barrier': 'bollard', 'bicyc...  LINESTRING (12.45828 41.9...

[3286 rows x 2 columns]
```

#### Just convert geometry to GeoParquet

```python
>>> import quackosm as qosm
>>> from shapely import from_wkt
>>> geometry = from_wkt(
...     "POLYGON ((14.4861 35.9107, 14.4861 35.8811, 14.5331 35.8811, 14.5331 35.9107, 14.4861 35.9107))"
... )
>>> gpq_path = qosm.convert_geometry_to_parquet(geometry)
>>> gpq_path.as_posix()
'files/4b2967088a8fe31cdc15401e29bff9b7b882565cd8143e90443f39f2dc5fe6de_nofilter_compact.parquet'
```

#### Inspect the file with duckdb

```python
>>> import duckdb
>>> duckdb.load_extension('spatial')
>>> duckdb.read_parquet(str(gpq_path)).order("feature_id")
┌──────────────────┬──────────────────────┬──────────────────────────────────────────────┐
│    feature_id    │         tags         │                   geometry                   │
│     varchar      │ map(varchar, varch…  │                   geometry                   │
├──────────────────┼──────────────────────┼──────────────────────────────────────────────┤
│ node/10001388317 │ {amenity=bench, ba…  │ POINT (14.5093988 35.8936881)                │
│ node/10001388417 │ {amenity=bench, ba…  │ POINT (14.5094635 35.8937135)                │
│ node/10001388517 │ {amenity=bench, ba…  │ POINT (14.5095215 35.8937305)                │
│ node/10018287160 │ {opening_hours=Mo-…  │ POINT (14.5184916 35.8915925)                │
│ node/10018287161 │ {defensive_works=b…  │ POINT (14.5190093 35.8909471)                │
│ node/10018287162 │ {defensive_works=h…  │ POINT (14.5250094 35.8883199)                │
│ node/10018742746 │ {defibrillator:loc…  │ POINT (14.5094082 35.8965151)                │
│ node/10018742747 │ {amenity=bank, nam…  │ POINT (14.51329 35.8991614)                  │
│ node/10032244899 │ {amenity=restauran…  │ POINT (14.4946298 35.8986226)                │
│ node/10034853491 │ {amenity=pharmacy}   │ POINT (14.4945884 35.9012758)                │
│       ·          │         ·            │               ·                              │
│       ·          │         ·            │               ·                              │
│       ·          │         ·            │               ·                              │
│ way/884730763    │ {highway=footway, …  │ LINESTRING (14.5218277 35.8896022, 14.5218…  │
│ way/884730764    │ {bridge=yes, highw…  │ LINESTRING (14.5218054 35.8896015, 14.5218…  │
│ way/884730765    │ {highway=footway, …  │ LINESTRING (14.5204069 35.889924, 14.52044…  │
│ way/884730766    │ {handrail=yes, hig…  │ LINESTRING (14.5204375 35.8898663, 14.5204…  │
│ way/884730767    │ {access=yes, handr…  │ LINESTRING (14.5196113 35.8906142, 14.5196…  │
│ way/884730768    │ {highway=steps, la…  │ LINESTRING (14.5197226 35.890676, 14.51972…  │
│ way/884730769    │ {access=yes, handr…  │ LINESTRING (14.5197184 35.8906707, 14.5197…  │
│ way/884738591    │ {highway=pedestria…  │ LINESTRING (14.5204163 35.8897296, 14.5204…  │
│ way/884744870    │ {highway=residenti…  │ LINESTRING (14.5218931 35.8864046, 14.5221…  │
│ way/884744871    │ {access=yes, handr…  │ LINESTRING (14.5221083 35.8864287, 14.5221…  │
├──────────────────┴──────────────────────┴──────────────────────────────────────────────┤
│ ? rows (>9999 rows, 20 shown)                                                3 columns │
└────────────────────────────────────────────────────────────────────────────────────────┘
```

#### Use as CLI

```console
$ quackosm --geom-filter-geocode "Shibuya, Tokyo"
100%|██████████████████████████████████████| 46.3M/46.3M [00:00<00:00, 327GB/s]
⠋ [   1/32] Reading nodes • 0:00:01
⠹ [   2/32] Filtering nodes - intersection • 0:00:00
⠋ [   3/32] Filtering nodes - tags • 0:00:00
⠋ [   4/32] Calculating distinct filtered nodes ids • 0:00:00
⠸ [   5/32] Reading ways • 0:00:03
⠴ [   6/32] Unnesting ways • 0:00:01
⠼ [   7/32] Filtering ways - valid refs • 0:00:00
⠹ [   8/32] Filtering ways - intersection • 0:00:00
⠋ [   9/32] Filtering ways - tags • 0:00:00
⠋ [  10/32] Calculating distinct filtered ways ids • 0:00:00
⠼ [  11/32] Reading relations • 0:00:00
⠸ [  12/32] Unnesting relations • 0:00:00
⠋ [  13/32] Filtering relations - valid refs • 0:00:00
⠋ [  14/32] Filtering relations - intersection • 0:00:00
⠋ [  15/32] Filtering relations - tags • 0:00:00
⠋ [  16/32] Calculating distinct filtered relations ids • 0:00:00
⠋ [  17/32] Loading required ways - by relations • 0:00:00
⠋ [  18/32] Calculating distinct required ways ids • 0:00:00
⠹ [  19/32] Saving filtered nodes with geometries • 0:00:00
⠋ [20.1/32] Grouping filtered ways - assigning groups • 0:00:00
⠼ [20.2/32] Grouping filtered ways - joining with nodes • 0:00:01
⠋ [20.3/32] Grouping filtered ways - partitioning by group • 0:00:00
  [  21/32] Saving filtered ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:00 < 0:00:00 •
⠋ [22.1/32] Grouping required ways - assigning groups • 0:00:00
⠼ [22.2/32] Grouping required ways - joining with nodes • 0:00:01
⠋ [22.3/32] Grouping required ways - partitioning by group • 0:00:00
  [  23/32] Saving required ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:00 < 0:00:00 •
⠴ [  24/32] Saving filtered ways with geometries • 0:00:00
⠋ [  25/32] Saving valid relations parts • 0:00:00
⠋ [  26/32] Saving relations inner parts • 0:00:00
⠋ [  27/32] Saving relations outer parts • 0:00:00
⠋ [  28/32] Saving relations outer parts with holes • 0:00:00
⠋ [  29/32] Saving relations outer parts without holes • 0:00:00
⠋ [  30/32] Saving filtered relations with geometries • 0:00:00
⠙ [  31/32] Saving all features • 0:00:00
⠋ [  32/32] Saving final geoparquet file • 0:00:00
Finished operation in 0:00:13
files/78580cf29b5ba1073366a257e1909bfeee43c9f5859e48fb3b2d592028bb58aa_nofilter_compact.parquet
```

### Let the QuackOSM automatically find the required OSM PBF file for you based on text query 🔎📄

#### Load data as a GeoDataFrame

```python
>>> import quackosm as qosm
>>> qosm.convert_osm_extract_to_geodataframe("Vatican City")
                                              tags                      geometry
feature_id
node/4227893563    {'addr:housenumber': '139', ...      POINT (12.45966 41.9039)
node/4227893564    {'amenity': 'fast_food', 'na...     POINT (12.45952 41.90391)
node/4227893565    {'name': 'Ferramenta Pieroni...     POINT (12.46042 41.90385)
node/4227893566    {'amenity': 'ice_cream', 'na...     POINT (12.45912 41.90394)
node/4227893568    {'amenity': 'cafe', 'name': ...     POINT (12.46112 41.90381)
...                                            ...                           ...
relation/2939617   {'building': 'yes', 'type': ...  POLYGON ((12.45269 41.908...
relation/11839271  {'building': 'yes', 'type': ...  POLYGON ((12.44939 41.897...
relation/12988851  {'access': 'private', 'ameni...  POLYGON ((12.45434 41.903...
relation/13571840  {'layer': '1', 'man_made': '...  POLYGON ((12.45132 41.899...
relation/3256168   {'building': 'yes', 'type': ...  POLYGON ((12.46061 41.907...

[8318 rows x 2 columns]
```

#### Just convert OSM extract to GeoParquet

```python
>>> import quackosm as qosm
>>> gpq_path = qosm.convert_osm_extract_to_parquet("Paris", osm_extract_source="OSMfr")
>>> gpq_path.as_posix()
'files/osmfr_europe_france_ile_de_france_paris_nofilter_noclip_compact.parquet'
```

#### Inspect the file with duckdb

```python
>>> import duckdb
>>> duckdb.load_extension('spatial')
>>> duckdb.read_parquet(str(gpq_path)).order("feature_id")
┌──────────────────┬────────────────────────────┬──────────────────────────────┐
│    feature_id    │            tags            │           geometry           │
│     varchar      │   map(varchar, varchar)    │           geometry           │
├──────────────────┼────────────────────────────┼──────────────────────────────┤
│ node/10000001235 │ {information=guidepost, …  │ POINT (2.3423756 48.8635788) │
│ node/10000001236 │ {barrier=bollard}          │ POINT (2.3423613 48.8635746) │
│ node/10000001237 │ {barrier=bollard}          │ POINT (2.3423555 48.8635657) │
│ node/10000001238 │ {barrier=bollard}          │ POINT (2.34235 48.8635575)   │
│ node/10000001239 │ {barrier=bollard}          │ POINT (2.3423438 48.8635481) │
│ node/10000005002 │ {amenity=vending_machine…  │ POINT (2.3438906 48.8642058) │
│ node/10000005003 │ {addr:city=Paris, addr:h…  │ POINT (2.3441257 48.8642723) │
│ node/10000005297 │ {emergency=fire_hydrant,…  │ POINT (2.2943897 48.8356289) │
│ node/10000034353 │ {name=Elisa&Marie, shop=…  │ POINT (2.3476407 48.8636628) │
│ node/10000079406 │ {emergency=fire_hydrant,…  │ POINT (2.2951077 48.8349097) │
│        ·         │         ·                  │              ·               │
│        ·         │         ·                  │              ·               │
│        ·         │         ·                  │              ·               │
│ node/10180452313 │ {highway=crossing}         │ POINT (2.2668596 48.8351167) │
│ node/10180457217 │ {amenity=charging_statio…  │ POINT (2.2996381 48.8654136) │
│ node/10180457222 │ {advertising=poster_box,…  │ POINT (2.2996126 48.8651971) │
│ node/10180457223 │ {advertising=poster_box,…  │ POINT (2.2990548 48.8651713) │
│ node/10180457224 │ {advertising=poster_box,…  │ POINT (2.3002578 48.8651435) │
│ node/10180457225 │ {advertising=poster_box,…  │ POINT (2.3001396 48.8649086) │
│ node/10180457226 │ {advertising=column, col…  │ POINT (2.3002337 48.8648869) │
│ node/10180457227 │ {advertising=poster_box,…  │ POINT (2.3004355 48.8648103) │
│ node/10180457247 │ {advertising=poster_box,…  │ POINT (2.3006468 48.8647237) │
│ node/10180457248 │ {advertising=poster_box,…  │ POINT (2.3008908 48.8643751) │
├──────────────────┴────────────────────────────┴──────────────────────────────┤
│ ? rows (>9999 rows, 20 shown)                                      3 columns │
└──────────────────────────────────────────────────────────────────────────────┘
```

#### Use as CLI

```console
$ quackosm --osm-extract-query "Gibraltar"
100%|█████████████████████████████████████| 1.57M/1.57M [00:00<00:00, 8.66GB/s]
⠙ [   1/32] Reading nodes • 0:00:00
⠋ [   2/32] Filtering nodes - intersection • 0:00:00
⠙ [   3/32] Filtering nodes - tags • 0:00:00
⠋ [   4/32] Calculating distinct filtered nodes ids • 0:00:00
⠙ [   5/32] Reading ways • 0:00:00
⠋ [   6/32] Unnesting ways • 0:00:00
⠹ [   7/32] Filtering ways - valid refs • 0:00:00
⠋ [   8/32] Filtering ways - intersection • 0:00:00
⠙ [   9/32] Filtering ways - tags • 0:00:00
⠋ [  10/32] Calculating distinct filtered ways ids • 0:00:00
⠙ [  11/32] Reading relations • 0:00:00
⠙ [  12/32] Unnesting relations • 0:00:00
⠼ [  13/32] Filtering relations - valid refs • 0:00:00
⠋ [  14/32] Filtering relations - intersection • 0:00:00
⠙ [  15/32] Filtering relations - tags • 0:00:00
⠙ [  16/32] Calculating distinct filtered relations ids • 0:00:00
⠙ [  17/32] Loading required ways - by relations • 0:00:00
⠙ [  18/32] Calculating distinct required ways ids • 0:00:00
⠙ [  19/32] Saving filtered nodes with geometries • 0:00:00
⠋ [20.1/32] Grouping filtered ways - assigning groups • 0:00:00
⠸ [20.2/32] Grouping filtered ways - joining with nodes • 0:00:10
⠙ [20.3/32] Grouping filtered ways - partitioning by group • 0:00:00
  [  21/32] Saving filtered ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:11 < 0:00:00 •
⠙ [22.1/32] Grouping required ways - assigning groups • 0:00:00
⠹ [22.2/32] Grouping required ways - joining with nodes • 0:00:12
⠙ [22.3/32] Grouping required ways - partitioning by group • 0:00:00
  [  23/32] Saving required ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:11 < 0:00:00 •
⠹ [  24/32] Saving filtered ways with geometries • 0:00:00
⠸ [  25/32] Saving valid relations parts • 0:00:00
⠋ [  26/32] Saving relations inner parts • 0:00:00
⠋ [  27/32] Saving relations outer parts • 0:00:00
⠙ [  28/32] Saving relations outer parts with holes • 0:00:00
⠙ [  29/32] Saving relations outer parts without holes • 0:00:00
⠹ [  30/32] Saving filtered relations with geometries • 0:00:00
⠹ [  31/32] Saving all features • 0:00:00
  [  32/32] Saving final geoparquet file 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16/16 • 0:00:00 < 0:00:00 • 163.96 it/s
Finished operation in 0:00:50
files/osmfr_europe_gibraltar_nofilter_noclip_compact.parquet
```

### Let the QuackOSM export data directly to a DuckDB database 🦆

#### Export existing PBF to a specific database file and table
```python
>>> import quackosm as qosm
>>> import duckdb
>>> monaco_pbf_path = "monaco.osm.pbf"
>>> ddb_path = qosm.convert_pbf_to_duckdb(
...    pbf_path=monaco_pbf_path,
...    result_file_path="monaco_osm.duckdb",
...    duckdb_table_name="osm"
... )
>>> ddb_path.as_posix()
'monaco_osm.duckdb'
>>> with duckdb.connect(str(ddb_path)) as con:
...     con.load_extension('spatial')
...     con.sql("SELECT * FROM osm ORDER BY feature_id;")
```

#### Export geometry to DuckDB
```python
>>> import quackosm as qosm
>>> from shapely import from_wkt
>>> wkt = (
...     "POLYGON ((7.41644 43.73598, 7.41644 43.73142, 7.42378 43.73142,"
...     " 7.42378 43.73598, 7.41644 43.73598))"
... )
>>> ddb_path = qosm.convert_geometry_to_duckdb(from_wkt(wkt))
>>> ddb_path.as_posix()
'files/bf4b33debfd6d3e605555340606df6ce7eea934958c1f3477aca0ccf79e7929f_nofilter_compact.duckdb'
```

#### Extract OSM and save to DuckDB
```python
>>> import quackosm as qosm
>>> ddb_path = qosm.convert_osm_extract_to_duckdb("monaco", osm_extract_source="geofabrik")
>>> db_path.as_posix()
'files/geofabrik_europe_monaco_nofilter_noclip_compact.duckdb'
```

#### Use as CLI
```console
$ quackosm monaco.osm.pbf --output monaco_osm.duckdb --duckdb-table-name 'osm'
⠇ [   1/32] Reading nodes • 0:00:00
⠋ [   2/32] Filtering nodes - intersection • 0:00:00
⠙ [   3/32] Filtering nodes - tags • 0:00:00
⠋ [   4/32] Calculating distinct filtered nodes ids • 0:00:00
⠴ [   5/32] Reading ways • 0:00:01
⠇ [   6/32] Unnesting ways • 0:00:00
⠴ [   7/32] Filtering ways - valid refs • 0:00:00
⠋ [   8/32] Filtering ways - intersection • 0:00:00
⠋ [   9/32] Filtering ways - tags • 0:00:00
⠋ [  10/32] Calculating distinct filtered ways ids • 0:00:00
⠸ [  11/32] Reading relations • 0:00:00
⠹ [  12/32] Unnesting relations • 0:00:00
⠋ [  13/32] Filtering relations - valid refs • 0:00:00
⠋ [  14/32] Filtering relations - intersection • 0:00:00
⠋ [  15/32] Filtering relations - tags • 0:00:00
⠋ [  16/32] Calculating distinct filtered relations ids • 0:00:00
⠋ [  17/32] Loading required ways - by relations • 0:00:00
⠋ [  18/32] Calculating distinct required ways ids • 0:00:00
⠸ [  19/32] Saving filtered nodes with geometries • 0:00:00
⠙ [20.1/32] Grouping filtered ways - assigning groups • 0:00:00
⠇ [20.2/32] Grouping filtered ways - joining with nodes • 0:00:03
⠧ [20.3/32] Grouping filtered ways - partitioning by group • 0:00:00
  [  21/32] Saving filtered ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:05 < 0:00:00 •
⠋ [22.1/32] Grouping required ways - assigning groups • 0:00:00
⠹ [22.2/32] Grouping required ways - joining with nodes • 0:00:02
⠋ [22.3/32] Grouping required ways - partitioning by group • 0:00:00
  [  23/32] Saving required ways with linestrings 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/1 • 0:00:01 < 0:00:00 •
⠸ [  24/32] Saving filtered ways with geometries • 0:00:01
⠹ [  25/32] Saving valid relations parts • 0:00:00
⠋ [  26/32] Saving relations inner parts • 0:00:00
⠋ [  27/32] Saving relations outer parts • 0:00:00
⠋ [  28/32] Saving relations outer parts with holes • 0:00:00
⠋ [  29/32] Saving relations outer parts without holes • 0:00:00
⠙ [  30/32] Saving filtered relations with geometries • 0:00:00
⠹ [  31/32] Saving all features • 0:00:00
  [  32/32] Saving final geoparquet file 100% ━━━━━━━━━━━━━━━━━━━━━━━━━ 12/12 • 0:00:01 < 0:00:00 • 9.85 it/s
Finished operation in 0:00:22
monaco_osm.duckdb
```

<details>
  <summary>CLI Help output (<code>QuackOSM -h</code>)</summary>

```console
 Usage: QuackOSM [OPTIONS] PBF file path

 QuackOSM CLI.
 Wraps convert_pbf_to_parquet, convert_geometry_to_parquet and convert_osm_extract_to_parquet
 functions and prints final path to the saved geoparquet file at the end.

╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│   pbf_file      PBF file path  PBF file to convert into GeoParquet. Can be an URL. [default: None]                                                                                                                                │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --osm-tags-filter                                                           TEXT (JSON)                   OSM tags used to filter the data in the JSON text form. Can take the form of a flat or grouped dict (look:              │
│                                                                                                           OsmTagsFilter and GroupedOsmTagsFilter). Cannot be used together with osm-tags-filter-file.                             │
│                                                                                                           [default: None]                                                                                                         │
│ --osm-tags-filter-file                                                      PATH                          OSM tags used to filter the data in the JSON file form. Can take the form of a flat or grouped dict (look:              │
│                                                                                                           OsmTagsFilter and GroupedOsmTagsFilter). Cannot be used together with osm-tags-filter.                                  │
│                                                                                                           [default: None]                                                                                                         │
│ --keep-all-tags,--all-tags                                                                                Whether to keep all tags while filtering with OSM tags. Doesn't work when there is no OSM tags filter applied           │
│                                                                                                           (osm-tags-filter or osm-tags-filter-file). Will override grouping if GroupedOsmTagsFilter has been passed as a filter.  │
│ --geom-filter-file                                                          PATH                          Geometry to use as a filter in the file format - any that can be opened by GeoPandas. Will return the unary union of    │
│                                                                                                           the geometries in the file. Cannot be used together with geom-filter-geocode or geom-filter-geojson or                  │
│                                                                                                           geom-filter-index-geohash or geom-filter-index-h3 or geom-filter-index-s2 or geom-filter-wkt.                           │
│                                                                                                           [default: None]                                                                                                         │
│ --geom-filter-geocode                                                       TEXT                          Geometry to use as a filter in the string to geocode format - it will be geocoded to the geometry using Nominatim API   │
│                                                                                                           (GeoPy library). Cannot be used together with geom-filter-file or geom-filter-geojson or geom-filter-index-geohash or   │
│                                                                                                           geom-filter-index-h3 or geom-filter-index-s2 or geom-filter-wkt.                                                        │
│                                                                                                           [default: None]                                                                                                         │
│ --geom-filter-geojson                                                       TEXT (GEOJSON)                Geometry to use as a filter in the GeoJSON format. Cannot be used used together with geom-filter-file or                │
│                                                                                                           geom-filter-geocode or geom-filter-index-geohash or geom-filter-index-h3 or geom-filter-index-s2 or geom-filter-wkt.    │
│