Agency client reporting with static maps and batch geocoding
Turn client location data into report-ready static maps in one API call. Batch geocode campaign responses and store visits for any client deck.
A table of campaign responses tells a client nothing. Forty rows of store addresses, postcodes, and response counts sit in an Excel sheet and communicate exactly one thing: that your agency collected the data. They do not communicate that you understand the geography, that you know which zip clusters are over-served, or that you can see the whitespace the client's competitors have not touched yet.
A labeled map of those same forty locations, dropped into the second slide of the deck, communicates all of that before the client has finished reading the title. It communicates "we know your market" faster than any table, any footnote, any two-paragraph executive summary.
This post is the full engineering walkthrough for that workflow. Batch geocode the client's raw location data against our 504M+ address database, generate a report-ready static map image with labeled pins, and ship the visual. All from one API key. No GIS licence, no cartography tool, no per-slide design work.
The agency problem with location data
Most marketing agencies handle location data awkwardly. The data arrives — a spreadsheet of store visits, a list of campaign response postcodes, a CSV of outlets in the client's distribution network — and the analyst does one of three things. They paste it into a free online mapping tool and screenshot the result. They send it to the GIS consultant who charges £150/hr and takes a week. Or they leave it as a table and hope the client does not ask for a map.
All three options are wrong for recurring client work. The screenshot approach does not scale, does not brand, and falls apart the moment the client asks for a version filtered by region or date. The GIS consultant is expensive and slow for a deliverable that should cost minutes of analyst time. The table approach is a slow leak on client confidence — the client gradually concludes that your agency is data-rich and insight-poor.
The right answer for recurring multi-client work is a repeatable, scriptable pipeline: take the client's CSV, geocode the addresses, generate a static map image per report section, and assemble the deck. Each step costs seconds of compute and a handful of API credits. The deliverable looks like your agency spent an afternoon with a cartographer. The client pays for insight, not for your tooling inefficiency.
What you are actually building
Three building blocks:
Batch geocoding via the web tool. Upload the client's CSV through the batch web interface. Credits are consumed per address row. You get back a CSV with latitude and longitude appended to every row that matched successfully. Unmatched rows come back flagged — you review them, correct the address, and re-run. The web tool is the right interface for the analyst who is not writing code; for the engineer building an automated pipeline, the REST endpoint is the same underlying service.
Static Maps API for report-ready images. One REST call per map image. Pass a centre coordinate, a zoom level, a set of pin markers with optional text labels, and an image size. You get back a PNG or JPEG you can drop directly into a Word document, a PowerPoint, a Figma frame, or a PDF. The image is ready to print; there is no "export this interactive map" step, no browser rendering, no screenshot automation.
The TEAM plan for shared client work. When more than one person at the agency is producing deliverables — the account manager pulls the geocoded CSV, the analyst generates the map, the senior strategist reviews the output — a TEAM plan puts everyone on a shared credit pool. The owner invites staff by email. Default is five seats. Everyone can use the web batch tool from their own login. No separate billing per seat, no per-user credit allocation to manage. One pool, one invoice. Details at csv2geo.com/pricing/api.
What you should not build
A brief honest constraint list, because the product boundaries matter for client conversations.
There is no white-label portal, no client-facing login, no reporting product. The map images and the geocoded coordinates are yours to use however you like in your deliverables. The client gets the slide, the PDF, the printed report — they do not get a login to a CSV2GEO dashboard. That is intentional: the deliverable belongs to your agency, not to the API.
There is no live interactive map in the API output. Static Maps produces a flat image. If the client wants an interactive map they can zoom and filter, that is a different product decision and a different engineering conversation.
And there is no white-labelling of the data pipeline itself. When a client asks "where did these coordinates come from?", the honest answer is "we geocode against a commercial address database as part of our standard data enrichment process." That is an accurate and complete answer. You do not need to name the provider; you should not pretend the coordinates came from your own proprietary database.
The batch geocoding workflow — step by step
Step 1: Prepare and clean the client's CSV
Before you upload anything, spend five minutes on the input data. The geocoder is forgiving of minor formatting inconsistencies — "St" versus "Street", missing suite numbers, inconsistent capitalisation — but it cannot recover from structurally broken address fields. A common agency problem: the client sends a spreadsheet where the street number, street name, city, and postcode are in four separate columns, or worse, concatenated in a single "notes" field.
Produce a single address column that reads: {number} {street}, {city}, {state_or_county}, {postcode}. For international addresses include the country at the end. This is not a strict requirement — the geocoder handles abbreviations and partial addresses — but a clean, consistent format produces higher confidence scores and fewer manual-review rows.
Remove duplicates before uploading. If the client's dataset has the same outlet address listed eight times (common with event-tracking data that records per-visit rather than per-location), deduplicate first and re-join after geocoding. You pay credits per row; duplicates are wasted spend.
Step 2: Upload via the web batch tool
Log in to the CSV2GEO web interface. From the batch geocoding tool, upload the prepared CSV. Select the column that contains the address. Run. The tool processes the file and returns a results CSV with lat, lng, confidence, and match_type appended to every input row.
Pay attention to the confidence column. A score below roughly 0.7 indicates a fuzzy match — the geocoder found something close but not certain. For a client deliverable, a low-confidence pin placed on a map can land a street away from the actual location, which a client with local market knowledge will notice immediately. Pull the low-confidence rows into a separate tab, review the addresses manually, and either correct and re-run or exclude them from the map with a note.
The match_type field tells you whether the geocoder matched to a specific address, to a street segment (interpolated), or to a postcode centroid. A postcode-centroid match places the pin at the geometric centre of the postcode area — visually this looks like all the responses clustered at the exact same point on the map, which is both wrong and visually misleading. Filter those out or flag them.
For a deeper treatment of what confidence scores actually mean and how to act on them, see Geocoding confidence scores explained.
Step 3: Generate a static map image per report section
With coordinates in hand, the Static Maps endpoint produces the image. A minimal curl call:
curl -G "https://csv2geo.com/api/v1/staticmap" \
--data-urlencode "center=51.5074,-0.1278" \
--data-urlencode "zoom=11" \
--data-urlencode "size=800x600" \
--data-urlencode "markers=51.5074,-0.1278|label:HQ" \
--data-urlencode "markers=51.5200,-0.0900|label:Store+1" \
--data-urlencode "markers=51.4900,-0.1500|label:Store+2" \
--data-urlencode "format=png" \
--data-urlencode "api_key=$CSV2GEO_API_KEY" \
-o "client_london_overview.png"The markers parameter accepts one marker per value: a coordinate pair and an optional label that renders as a text callout on the pin. For a campaign-response map with forty locations you repeat the markers parameter forty times, or build the URL programmatically — which is what the Python and Node examples below do.
In Python with requests:
import csv
import os
import requests
API = "https://csv2geo.com/api/v1/staticmap"
KEY = os.environ["CSV2GEO_API_KEY"]
def build_map(rows, output_path, center_lat, center_lng, zoom=11, size="800x600"):
params = [
("center", f"{center_lat},{center_lng}"),
("zoom", str(zoom)),
("size", size),
("format", "png"),
("api_key", KEY),
]
for row in rows:
label = row.get("label", "").replace(" ", "+")
marker_val = f"{row['lat']},{row['lng']}|label:{label}" if label else f"{row['lat']},{row['lng']}"
params.append(("markers", marker_val))
r = requests.get(API, params=params, timeout=30)
if r.status_code == 400:
print(f"Bad request — check coordinates or parameter format: {r.text}")
return None
r.raise_for_status()
with open(output_path, "wb") as f:
f.write(r.content)
return output_path
with open("client_geocoded.csv") as fin:
rows = list(csv.DictReader(fin))
# Filter out low-confidence and postcode-centroid matches before mapping
mappable = [r for r in rows if float(r["confidence"]) >= 0.7 and r["match_type"] != "postcode"]
result = build_map(
mappable,
output_path="client_map_overview.png",
center_lat=51.5074,
center_lng=-0.1278,
zoom=11,
)
print(f"Map saved to: {result}")In Node with fetch:
import { writeFile } from 'node:fs/promises';
import { createReadStream } from 'node:fs';
const API = 'https://csv2geo.com/api/v1/staticmap';
const KEY = process.env.CSV2GEO_API_KEY;
async function buildMap(rows, outputPath, centerLat, centerLng, zoom = 11, size = '800x600') {
const params = new URLSearchParams({
center: `${centerLat},${centerLng}`,
zoom: String(zoom),
size,
format: 'png',
api_key: KEY,
});
for (const row of rows) {
const label = (row.label || '').replace(/ /g, '+');
const markerVal = label
? `${row.lat},${row.lng}|label:${label}`
: `${row.lat},${row.lng}`;
params.append('markers', markerVal);
}
const r = await fetch(`${API}?${params.toString()}`);
if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`);
const buf = Buffer.from(await r.arrayBuffer());
await writeFile(outputPath, buf);
return outputPath;
}Note: SDKs exist for both Python and Node — but the REST calls above are stable, version-independent, and easier to audit in a CI pipeline. We recommend the direct HTTP approach for any code that needs to run unattended in a reporting job.
Step 4: Produce per-region maps for multi-section reports
A single overview map works for a one-page executive summary. A proper client report has sections — one per region, one per campaign, one per competitor territory. Each section needs its own map, framed to the right area.
The pattern: group the geocoded rows by the segmentation dimension (region, campaign name, date range), compute a bounding box from the lat/lng of each group, derive a centre and zoom level, and generate one image per group.
A simple bounding-box-to-zoom helper:
import math
def bbox_zoom(lats, lngs, image_px=800):
lat_span = max(lats) - min(lats)
lng_span = max(lngs) - min(lngs)
span = max(lat_span, lng_span)
if span == 0:
return 15 # single point
zoom = math.floor(math.log2(360 / span)) + 1
return min(max(zoom, 3), 18) # clamp to sensible range
def bbox_center(lats, lngs):
return (sum(lats) / len(lats), sum(lngs) / len(lngs))For each region group:
from itertools import groupby
rows_sorted = sorted(mappable, key=lambda r: r["region"])
for region, group in groupby(rows_sorted, key=lambda r: r["region"]):
group = list(group)
lats = [float(r["lat"]) for r in group]
lngs = [float(r["lng"]) for r in group]
clat, clng = bbox_center(lats, lngs)
zoom = bbox_zoom(lats, lngs)
build_map(
group,
output_path=f"client_map_{region.lower().replace(' ', '_')}.png",
center_lat=clat,
center_lng=clng,
zoom=zoom,
)Each PNG drops into the relevant section of the report. The analyst assembles the deck; the engineer does not need to touch PowerPoint.
Step 5: Keep client data separated and exports clean
This is the operational hygiene step that most pipelines skip until something goes wrong. When you are running geocoding jobs for multiple clients under a shared TEAM plan, the credit pool is shared but the data must not be. Do not mix client data into a single geocoding batch — run separate uploads per client, download separate result CSVs per client, and store them in separate directories or S3 prefixes per client.
The reason is audit, not just compliance. When a client asks "can you show me exactly what data you processed about our locations?", you need to produce a clean, client-specific export without spending an afternoon filtering a mixed dataset. If a data subject rights request ever arrives (GDPR-adjacent, depending on whether any of the location data links to individuals), you need to be able to produce or delete exactly the right rows without touching another client's data.
The TEAM plan's shared credit pool does not merge the underlying data — that is your responsibility to manage at the storage and workflow level. Treat each client as a separate project folder. One batch upload per client. One geocoded CSV per client. One set of map images per client. This also makes re-running or updating a client deliverable straightforward when their data changes.
Fitting this into a repeatable agency workflow
The goal is a workflow that an analyst can run in thirty minutes for a new client brief, and in ten minutes for a returning client update. That means three things.
Template the pipeline, not the output. Write the Python or Node script once, parameterise it by client name, input CSV path, report title, and output directory. The map images are generated fresh per run; the code does not change. Keep the script in the agency's shared code repository. Version-control it. When a new analyst joins the team, they should be able to produce a client map on their first day.
Cache geocoding results. Addresses do not move. If the client sends you the same outlet list again next quarter with minor additions, you should not re-geocode the rows you already have coordinates for. Store the geocoded CSV with a hash of the input address as the lookup key. Re-run only the rows that are genuinely new or changed. This cuts credit spend significantly on recurring deliverables and keeps your quarterly cost predictable. For the detailed caching pattern see Caching geocoding results — 90% cost reduction.
Build the confidence-review step into the workflow, not as a bolt-on. Low-confidence and postcode-centroid matches are not errors — they are signals that the address data needs attention. Build a step in your pipeline that produces a review_required.csv alongside the main geocoded output. Any row with confidence below 0.7 or match type of postcode lands there. The analyst reviews it, corrects the address if possible, and re-runs those rows. This step takes five minutes and saves the embarrassment of a pin landed on the wrong side of a city boundary in a client presentation.
Cost math for a real agency engagement
A typical marketing agency runs twelve to twenty active clients at any time. Each client engagement involves a geocoding job of perhaps 200 to 2,000 address rows per quarter, plus three to eight static map images per report, per reporting cycle.
Take the middle of that range: fifteen clients, 600 rows each per quarter, six static map images per client per quarter.
- Geocoding: 15 × 600 = 9,000 credits per quarter
- Static maps: 15 × 6 = 90 credits per quarter
- Total per quarter: 9,090 credits
- Annualised: ~36,000 credits
At the entry paid tier — $54/month for 100,000 calls — a full year of that workload is comfortably inside a single monthly allocation. You are not buying an additional tier; you are using a fraction of the base plan.
For larger engagements (a national retailer with 5,000 outlets, a political campaign with county-level coverage across a whole state), the per-credit economics stay the same — the volume goes up, the unit cost goes down as you move through tiers. All live pricing brackets are at csv2geo.com/pricing/api; no quote process, published rates.
The free tier (3,000 calls per day, no credit card) is sufficient to pilot the workflow end-to-end for a single client engagement before committing to a paid plan.
What makes a static map credible in a client deck
A few cartographic opinions worth holding.
Label only the pins that need labelling. If you have forty retail locations and you label all forty, the map reads as visual noise. Label the top five by sales, or by response rate, or by the metric the client cares about. Use the label field for the thing that makes a pin notable, not for an address.
Use zoom to tell a story, not to fit everything in. A map zoomed out to show a whole country with forty pins looks like a scatter of dots. A map zoomed to the region where the whitespace opportunity is — even if it only shows twelve of the forty pins — makes the point sharper. Generate multiple maps per report at different zoom levels rather than one map that tries to say everything.
Keep the image dimensions report-ready. A 16:9 slide at standard resolution is 1920×1080 px. A half-slide map placeholder is typically around 900×500 px. Request the image at those dimensions directly from the API rather than scaling a 400×300 image up in PowerPoint — upscaled small images look unprofessional in a client meeting on a large screen.
The map is evidence, not decoration. Every pin in the client's slide should mean something to the client's business question. "Here is where your campaign responses clustered and here is the gap your competitor has not filled" is a map. "Here are all your stores" is a directory. Do not confuse the two.
Observability for the pipeline
When this becomes a recurring, automated pipeline rather than a manually-run script, instrument it. Track credits consumed per client, per run, per week. An unexpected spike in credit spend is almost always explained by a data-quality regression upstream — the client sent a new export with a column alignment error that produced garbage address strings, and the geocoder matched them all to postcode centroids rather than failing cleanly.
See Observability for geocoding pipelines — metrics that matter for the full metric taxonomy. The short version for an agency pipeline: track confidence_score_mean, match_type_postcode_pct, credits_consumed_per_row, and rows_to_review_pct per run. When any of those drift outside their normal band, you want an alert before the next client deliverable runs, not after.
Frequently Asked Questions
Can one TEAM plan account handle geocoding for multiple clients simultaneously? Yes. The TEAM plan's shared credit pool has no restriction on how many different datasets you process. The responsibility for keeping client data separated — running separate batch jobs, downloading separate result files, storing data in per-client directories — is yours as the operator. The plan does not mix your clients' data; your pipeline should not either.
How many seats does the TEAM plan include by default? The default is five seats. The owner of the account invites team members by email. All seats share the same credit pool. The web batch tool is available to all seats; there is no per-seat permission system beyond the owner/member distinction.
What happens if the client's address data is in a non-English locale or uses local address formats? The geocoder covers 63 countries and handles a wide range of local address formats and character sets. For international client data — a European retailer's store list, a Latin American campaign response dataset — include the country in the address field and the geocoder routes to the appropriate regional resolution logic. Confidence scores still apply; review low-confidence rows before mapping.
Can I automate the static map generation so it runs without analyst involvement? Yes. The REST endpoint is fully scriptable. A cron job or CI pipeline step that reads a geocoded CSV and produces a set of PNG files is a few dozen lines of Python or Node. The images land in a shared drive or S3 bucket and the analyst picks them up for the deck. You do not need a human in the loop between geocoding and image generation.
Is there a limit on how many pins I can add to a single static map? The endpoint accepts multiple markers parameters per call. For practical readability, maps with more than roughly fifty to sixty pins become visually crowded at standard report sizes. If you have a dataset with several hundred locations, consider generating multiple maps — one per region, or a heat-density representation — rather than a single pin-saturated image. The API does not impose a hard pin-count limit, but the map's usefulness to the client does.
What should I tell a client who asks about the geocoding data sources? The accurate and complete answer is: "We geocode your location data against a commercial address database covering 504 million addresses across 63 countries, and we produce coordinates via REST API call." You do not need to name the provider if you prefer not to; "commercial address geocoding API" is sufficient for most client conversations. If a client has specific data-residency or data-processing questions, point them to the privacy documentation and data-processing terms at csv2geo.com.
Does each static map call consume one credit regardless of the number of pins? Yes. One API call produces one image, and that image counts as one credit regardless of how many marker pins you include. Geocoding credits are separate — they are consumed during the batch geocoding step, one per address row processed.
Related Articles
- Benchmarking geocoding APIs — honest numbers — how to measure geocoding quality for your specific client data types, not just average match rates
- Caching geocoding results — 90% cost reduction — why recurring client work should cache geocoded coordinates rather than re-processing the same addresses each quarter
- Observability for geocoding pipelines — metrics that matter — the metrics that tell you when an upstream data-quality problem is about to break your client deliverable
- Concurrency tuning for geocoding pipelines — finding the sweet spot — how to size parallel geocoding jobs when you are processing multiple client datasets simultaneously
- Geocoding confidence scores explained — what the confidence field actually means and how to act on low-confidence matches before they land on a client map
---
*I.A. / CSV2GEO Creator*
Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.
Try Batch Geocoding Free →