# GeoParquet to PMTiles Converter (`madrasco/geoparquet-to-pmtiles`) Actor

Convert one or two uploaded WGS84 polygon GeoParquet layers into a PMTiles vector archive. Download tiles and a report of selected attributes and source IDs missing at each zoom.

- **URL**: https://apify.com/madrasco/geoparquet-to-pmtiles.md
- **Developed by:** [Jack Valmadre](https://apify.com/madrasco) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / converted polygon map

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## GeoParquet to PMTiles Converter

Convert one or two uploaded polygon GeoParquet layers into one downloadable PMTiles v3 archive containing MVT vector tiles. Use it for small static vector-map layers and Apify workflows without installing a local tiling toolchain.

You get `MAP.pmtiles` and an `OUTPUT` JSON report with tile counts, selected attributes, source IDs missing at each requested zoom, and the archive's SHA-256 hash. Each completed run also writes the report as one dataset row.

This Actor accepts WGS84 polygons only, with a combined limit of 16 MiB and 2,000 features. Tiling simplifies geometry and small polygons can disappear. It provides a downloadable archive; styling and permanent map hosting are outside its scope.

### Upload and convert

1. Use the official Apify Python client to create a key-value store and upload an **uncompressed** binary record, as shown below. Set `content_encoding="identity"`: the client's default compressed upload is unsupported. Keep the store ID and record key, such as `areas.parquet`.
2. Turn **Use bundled lake example** OFF. It is on by default and uses a Natural Earth lake instead of your files.
3. Select the uploaded-file store, confirm rights to process nonpersonal data, and describe each layer. Choose zooms suitable for your layer's extent.
4. Start the run. Download the archive after the run is **SUCCEEDED** and `OUTPUT.status` is `success`. Check the downloaded file's SHA-256 against `OUTPUT.sha256` before using it.

Upload with the official `apify-client` 3.2.1 Python package and your own Apify API token in the `APIFY_TOKEN` environment variable:

```python
import os
from pathlib import Path
from apify_client import ApifyClient

client = ApifyClient(token=os.environ["APIFY_TOKEN"])
store = client.key_value_stores().get_or_create(name="my-map-inputs")
client.key_value_store(store.id).set_record(
    "areas.parquet",
    Path("areas.parquet").read_bytes(),
    content_type="application/octet-stream",
    content_encoding="identity",
)
print(store.id)
```

Use that printed ID as `inputStoreId`. This API route is tested; a Console upload route is not verified. See Apify's [create-store API](https://docs.apify.com/api/v2/key-value-stores-post) and [upload-record API](https://docs.apify.com/api/v2/key-value-store-record-put). HTTP-compressed records are refused before conversion. Parquet's internal column compression is supported and is a separate input limit.

For a file with a unique `area_id` column and a scalar `name` column:

```json
{
  "useDemo": false,
  "inputStoreId": "YOUR_STORE_ID",
  "rightsConfirmed": true,
  "layers": [
    {
      "name": "areas",
      "recordKey": "areas.parquet",
      "idColumn": "area_id",
      "attributes": ["name"]
    }
  ],
  "minZoom": 0,
  "maxZoom": 4
}
```

For small local building footprints, zooms 12 to 14 can be more suitable than the generic 0 to 4 example. Choose zooms for your own extent and check the omission report.

For two layers, add a second object to `layers` with a distinct `name`. Both files must be in the selected store. Output features include `source_id` as a string, plus your selected attributes. IDs must be unique within each input layer; output fragments can repeat the same source ID across tiles.

Open the downloaded archive locally in a PMTiles-compatible viewer, such as [pmtiles.io](https://pmtiles.io/), to check its appearance. In MapLibre, set `source-layer` to the output layer name shown in `vector_layers[].id`. Vector tile coordinates use Web Mercator (EPSG:3857); input and archive bounds use longitude/latitude. `source_id` is an attribute, not a unique MVT feature ID.

### Inputs and limits

- GeoParquet 1.0 or 1.1 with binary WKB geometry, a declared Polygon or MultiPolygon geometry type, planar edges and WGS84 longitude/latitude coordinates. Projected or undefined CRS, points, lines and 3D geometry are unsupported.
- One or two layers, 16 MiB combined file size, 2,000 total features and 750,000 coordinate vertices. These are input limits, not a promise that every admitted request completes.
- A unique, nonempty string or integer ID per feature. Up to eight selected scalar attributes per layer: strings, booleans and finite numbers, with null values allowed. Dates and nested values are unsupported. Selected integers must fit the exact JSON integer range; use the ID column for larger integer identifiers.
- Zooms 0 to 14 and at most 5,000 potential tiles. The estimate uses each layer's bounding box with a buffer, so wide extents can be refused even for small files.
- At most 64 MiB declared uncompressed Parquet data, 16 MiB final archive, 256 MiB scratch space checked at intervals and ten minutes processing. Verification can also refuse unusually large fields or tile fragments.

An unsupported or over-budget request returns `OUTPUT.status: refused` with a reason and no archive. Storage or lifecycle failures can fail the run without a completion report. A report alone does not confirm completion: require the successful run and matching archive hash.

### What the report checks

The converter reads the PMTiles directory and each addressed output tile using PMTiles and MVT libraries. It checks tile addresses, decoding, polygon fragment validity and winding, source IDs and selected attribute values. The report lists retained and missing source IDs separately for every requested zoom.

These checks describe the generated tiles. They do not certify geographic accuracy, seam appearance or lossless geometry. Tippecanoe clips, simplifies and quantizes polygons; coordinates near the poles are clamped to Web Mercator. Use your own map preview before relying on the appearance.

### Price

US$0.25 per completed uploaded-file archive, whether it contains one or two layers, plus Apify's standard US$0.00005 start charge per GB of run memory. At the default 2 GiB memory setting, a successful uploaded-file conversion costs US$0.25010 in total. Apify usage for the run is included. Your own key-value store with the uploaded files follows your Apify plan. Set your run's maximum total charge to at least US$0.251.

The bundled example and refused requests have no conversion fee; the start charge still applies. The conversion charge is attempted only after the archive, report and dataset row are stored. A late billing or lifecycle failure can leave a FAILED run with a charged archive retained. Contact support before starting again in that case. Resurrection of the same run checks its billing record and retained archive without a second conversion charge. Resurrection can add another Apify start charge. A new run is a new job and can incur a new charge.

### Files and privacy

Use only geographic data you have permission to process and that contains no personal records. This Actor reads your uploaded records and writes new outputs without changing the source files. It does not fetch arbitrary source URLs. Your files and outputs are stored on Apify under your storage permissions and retention settings. Deleting the run or its storage can remove access to outputs.

### FAQ

**Can I use it with a large national dataset?** Only if your supplied layer fits all limits. This version serves small polygon jobs; it is not a bulk-data service.

**Does it include a map viewer or hosting?** No. Download the archive for a PMTiles-compatible map workflow and configure its styling and hosting separately.

**Can I do this locally?** Yes. Free local conversion tools are available. This Actor packages a bounded conversion job and a tile report for Apify workflows.

**Where can I get help?** Use this Actor's Issues tab or support@madrasco.dev. Platform replies may be AI-assisted; the human owner is available on request.

Built and maintained by Madrasco with AI agents. AI agents wrote the code and this page; scripts perform the conversion checks. Excludes safety-critical use, including navigation and survey decisions.

### Changelog

- 0.1: initial release: one or two uploaded polygon layers, PMTiles vector output and per-zoom source-ID reporting.

# Actor input Schema

## `useDemo` (type: `boolean`):

On by default: run the bundled Natural Earth lake example. Turn this OFF to process your own uploaded records; supplying an input store while this is on is refused.

## `inputStoreId` (type: `string`):

The Apify key-value store containing your binary GeoParquet uploads. Select it here to grant this run READ access. Use a different store from the run output store.

## `layers` (type: `array`):

One or two objects with name (output tile layer), recordKey (uploaded filename), idColumn (unique string or integer ID), and attributes (up to eight scalar column names). Ignored in demo mode.

## `rightsConfirmed` (type: `boolean`):

Required for your own uploads. Confirm permission to process nonpersonal geographic data. Do not use this Actor for safety-critical work. The bundled demo needs no confirmation.

## `minZoom` (type: `integer`):

Lowest output zoom, 0 to 14. Must not exceed maximum zoom. Default: 0.

## `maxZoom` (type: `integer`):

Highest output zoom, 0 to 14. Default: 4. Larger extents at high zoom can exceed the 5,000 potential-tile limit even when the file is small.

## Actor input object example

```json
{
  "useDemo": true,
  "layers": [
    {
      "name": "polygons",
      "recordKey": "layer.parquet",
      "idColumn": "id",
      "attributes": []
    }
  ],
  "rightsConfirmed": false,
  "minZoom": 0,
  "maxZoom": 4
}
```

# Actor output Schema

## `report` (type: `string`):

No description

## `archive` (type: `string`):

No description

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "layers": [
        {
            "name": "polygons",
            "recordKey": "layer.parquet",
            "idColumn": "id",
            "attributes": []
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("madrasco/geoparquet-to-pmtiles").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "layers": [{
            "name": "polygons",
            "recordKey": "layer.parquet",
            "idColumn": "id",
            "attributes": [],
        }] }

# Run the Actor and wait for it to finish
run = client.actor("madrasco/geoparquet-to-pmtiles").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "layers": [
    {
      "name": "polygons",
      "recordKey": "layer.parquet",
      "idColumn": "id",
      "attributes": []
    }
  ]
}' |
apify call madrasco/geoparquet-to-pmtiles --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,madrasco/geoparquet-to-pmtiles"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/thbCbu5UFESfDsURS/builds/JkugKAstnDOSY23NZ/openapi.json
