Batch geocoding
Every request needs an API key; see Authentication.
There are three ways to geocode many addresses, and only the first two are API surfaces.
POST /geocode/batch is the API: up to 100 searches, one HTTP request, one billed call per
non-blank query. There is a copy-paste example.
unmap geocode csv is a client of that same route for a file on your machine. The file never
leaves your machine; only the mapped column is sent.
Hosted batch is the third, and it is a dashboard feature rather than an API: you upload a CSV at unmap.dev/dashboard/batch, unmap holds it while the job runs and for a published retention window, and you download the annotated copy. It is session-authenticated, so an API key cannot reach it and there is no SDK or CLI for it. See Hosted batch below.
The API
POST /geocode/batch
The body is JSON. queries is required: 1 to 100 strings, order preserved. Shared search
controls only (lang, limit, layers, focus, region, bbox). The response is an array
aligned to queries. Index i is what GET /geocode/search would return for that string.
Blank entries stay as [] and are not billed. More than 100 queries is a 413. Remaining
allowance below the non-blank count refuses the whole request.
curl -X POST "https://api.unmap.dev/geocode/batch" \
-H "Authorization: Bearer $UNMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"queries":["17 Ave SW Calgary","Springfield"],"limit":1}'import { Geocoder } from "@unmap/geocoding";
const geocoder = new Geocoder({ key: "um_live_..." });
const results = await geocoder.batch(["17 Ave SW Calgary", "Springfield"], { limit: 1 });geocoder.batch chunks a longer list at 100. A 100,000-row file is 1,000 client requests, not
a Worker queue. The Geocoding API has the full contract.
A CSV on your machine
unmap geocode csv reads the file, POSTs the mapped column in chunks of 100, and writes a
copy with ten new columns appended. Your file is never modified and never uploaded.
npx @unmap/cli geocode csv addresses.csv --query-column address --output geocoded.csv --review-output review.csvDownload a sample file to try it on. It is twenty synthetic rows covering the cases that are worth seeing: accents, a street with no civic number, a rural route, the same street name in two provinces, a blank row, and one address that cannot be resolved at all.
Only the column you map leaves the machine. The columns you did not map are never sent. Your
API key comes from --key, the UNMAP_KEY environment variable, or unmap.config.json. It is
never written to the output, the review file, the resume record, or any message the tool prints.
Count before you spend
--estimate validates the mapping and counts the rows without sending a single request.
npx @unmap/cli geocode csv addresses.csv --query-column address --estimate20 rows, 19 with an address, 1 without.
1 requests if nothing retries; up to 4 if every request uses all 4 attempts.
Each request geocodes up to 100 addresses. Every successful address is one billed call. Retries are billed.
Remaining allowance: unknown from the CLI. Check the dashboard at unmap.dev/dashboard/usage.
Two numbers, not one. Requests are POSTs of up to 100 addresses. Each successful address is one
call against your plan. A retried request is billed again, the same as retrying
GET /geocode/search. A row with no address is zero. The estimate reports the floor and the
ceiling and says which is which.
--sample 100 runs only the first hundred rows with an address, then stops and checkpoints. It
does send requests, and they are billed. Re-run with --resume and no --sample to finish.
The columns it adds
Ten columns, all prefixed unmap_, appended after your own. If your file already has a column by
one of these names it is suffixed (unmap_status_2) rather than overwritten, and the tool says so.
| Column | Meaning |
|---|---|
unmap_status | matched, review, no_match, invalid, failed, or unprocessed |
unmap_id | Stable id of the matched record |
unmap_name | The label the geocoder resolved |
unmap_lng, unmap_lat | Coordinates, WGS84 |
unmap_layer | address, street, locality, region, or poi |
unmap_source | Which dataset the record came from |
unmap_match_type | exact, partial, fallback, or unknown |
unmap_precision | point, street, locality, region, or unknown |
unmap_review | Why the row needs a look, when it does |
unmap_status and unmap_match_type answer different questions, and the pairing worth watching
for is matched with fallback. That means an answer came back and it is coarser than the row
asked for: you gave a civic number and got the street. See
result metadata.
The review file
--review-output writes a second CSV holding only the rows worth looking at: fallbacks, partial
matches, near-ties between the top two candidates, rows with no match, and rows that failed. It
is a subset of the main output, in the same format, with the same columns.
Every input row appears exactly once in the main output no matter what happened to it. Nothing is dropped quietly, which is the point of having a status column at all.
Interrupt it
Press Ctrl-C and the run stops after the requests already in flight, then writes a resume record
next to the output. Run the same command again with --resume and it continues from the last row
it durably wrote.
npx @unmap/cli geocode csv addresses.csv --query-column address --output geocoded.csv --resumeThe resume is refused if the input file or the mapping has changed, because row numbers would no
longer line up and half the file would have been geocoded a different way. Changing the pace
(--rate, --concurrency, --max-attempts) is fine: those change how hard the tool tries, not
what any row resolves to.
One thing resume cannot do: a request that was in flight when the process died may already have been served, and it will be sent again. Nothing on your machine can know which, so a resumed run can bill a small number of rows twice.
Pace
| Flag | Default | What it bounds |
|---|---|---|
--rate | 10/second | How fast requests are sent, averaged |
--concurrency | 4 | How many are in flight at once |
--max-attempts | 4 | Attempts per request, including the first |
Rate and concurrency bound different things and are set separately. The defaults are deliberately well under the per-key burst limit, because a batch run shares your account's allowance with whatever you are serving to real users. See plans and limits.
Retries are only for failures that could succeed later: a rate limit, a timeout, a 5xx. A
malformed request is never retried. A bad key stops the whole run immediately and marks no
address as bad, because the address was not the problem. An exhausted quota or spend cap also
stops the run rather than retrying into it; re-run with --resume once it lifts.
Spreadsheets
--spreadsheet-safe prefixes a tab onto any value that Excel, Sheets or LibreOffice would
evaluate as a formula, so a cell beginning =, +, - or @ displays as text. It is off by
default because it changes the bytes, and a pipeline reading the file programmatically wants the
original value.
What it does not do
- It geocodes free text. Column mapping for separate address, city and province fields uses structured search and is not wired into the CLI yet.
- It does not validate deliverability. A postal code in a row is compared against whatever the matched record carries; unmap does not know whether an address receives mail.
- Canada only.
POST /geocode/batch is not a job API
POST /geocode/batch answers in the same HTTP call. It returns results, not a job id; unmap does
not hold anything for it, does not queue it, and does not take a webhook. Everything on this page
above this heading describes that request.
Hosted batch, in the dashboard
There is an async job runner, and it is the only place on the platform where unmap stores a customer file. It is not an API: it authenticates with your browser session, so an API key cannot reach it, and there is no SDK method or CLI command for it.
Use it from unmap.dev/dashboard/batch. The flow is upload, choose the address column, read the estimate, then Start geocoding. Nothing is billed before that button.
| Plans | Paid plans only. The free plan is for development. |
| Size | 10,000 rows / 25 MiB per file on indie; 100,000 rows / 50 MiB on growth. |
| Concurrency | One running job on indie, two on growth. |
| Billing | One call per eligible row, from the same allowance maps and routing draw on. A row with a value in the address column costs one call even when we find no match; a row with no value costs nothing. Platform retries never add a charge. |
| Progress | The job keeps running after you close the page. |
| Cancel and resume | Both. A job stopped by a spend cap resumes once the cap is raised. |
| Partial failure | A job that finishes with rows to review says so, and the download separates them. |
Retention. unmap holds the uploaded file, its parsed chunks, the results and the exports for seven days from job creation, then deletes them. The exact timestamp is shown before you upload and on the job itself; it is not extended by a retry or a resume. Content-free job status and counts are kept for thirty days. The privacy policy states the same window. Download your copy rather than treating the hosted one as storage. unmap does not claim Canadian data residency on any self-serve plan, and hosted batch does not change that: see Privacy for where processing happens.
A generic job API is still later work
Hosted batch is a batch-geocoding feature, not a platform job API. A generic one is later work
and will be async in the same shape: POST /jobs returns 202 and a job id, the client polls
GET /jobs/:id, and the create request does not wait for the work. The trigger is the first
product that cannot finish in one request: a large routing matrix, many truck routes, anything
past a Worker or container budget. That API will be a new prefix and new tables rather than an
extension of hosted CSV, and it will be key authenticated. Geocoding does not get a job handler
in that first cut. Do not build it to make a spreadsheet faster.
Next steps
- Geocoding API for the search this batches, one query at a time.
- Geocode many addresses is the request as a copy-paste example.
- Plans & Limits for what a large batch costs against your quota.
- Privacy for how long a hosted batch file is kept.