Batch geocoding
You have a spreadsheet of addresses and you want coordinates next to them. unmap geocode csv
reads the file, sends one column to the geocoding API, and writes a copy with ten new columns
appended. Your file is never modified.
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.
In the dashboard
If you would rather not run it locally, batch geocoding in the dashboard does the same job on our side: upload, choose the column, look at what it will cost, press start. The job keeps running after you close the tab.
The two produce the same file, with the same columns in the same order, so you can prototype with the CLI and move the job to the dashboard without anything downstream noticing.
Three differences worth knowing:
- A paid plan is required. The free plan covers development against localhost; processing a production file on our machines is paid work.
- Retries never cost you anything. The hosted service bills one call per eligible row that
reaches an answer, no matter how many times it had to try. The CLI retries over HTTP, so there
each attempt is billed, which is why
--estimatequotes a ceiling and the dashboard does not. - Your file is stored while the job runs. It is deleted seven days after upload, and the exact date is shown before you upload and on the job page. The CLI never sends the file at all.
Limits per job, by plan:
| indie | growth | |
|---|---|---|
| Rows per job | 10,000 | 100,000 |
| File size | 25 MB | 50 MB |
| Jobs processing at once | 1 | 2 |
| Unfinished jobs | 5 | 10 |
| Retained files | 250 MB | 1 GB |
These bound what hosting the work costs, not what you are allowed to spend. Rows are billed as ordinary calls against the same monthly allowance your maps and routes draw on.
What leaves your machine
Only the column you map. The file stays where it is, the columns you did not map are never sent, and the output and the resume record are written next to your input and are never uploaded anywhere.
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.
19 requests if nothing retries; up to 76 if every row uses all 4 attempts.
One request is one API call against your plan's shared allowance. Retries are billed.
Remaining allowance: unknown from the CLI. Check the dashboard at unmap.dev/dashboard/usage.
Two numbers, not one, because one input row is not one billable request. A row that fails transiently and succeeds on its third attempt is three calls. 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 row, 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 job 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.