If you have ever built a "counts by state" page on top of OpenStreetMap data, you learn quickly that the hard part is not the query. It is deciding what a count means, and then making every derived number carry that meaning with it.
This is a write-up of the invariants I found useful. No numbers here, because any total I printed would be stale by the time you read it.
Start with the tag contract, not the map
The source is a shared, volunteer-maintained dataset. Contributors place a node at the physical location of a device and tag it as a surveillance feature, with a further tag identifying the device type as ALPR so it can be separated from generic CCTV. Where someone could tell, they also record the manufacturer, the compass bearing, the mounting, and a check date.
That is a loose schema. Fields are optional, values are free text, and contributors do not coordinate. Your pipeline has to assume that any field can be missing or malformed. A record with no bearing is still a valid record. A record with a free-text manufacturer you cannot normalize is still a valid record. Do not filter those out unless you are explicit that your page shows a subset.
The data methodology page for this map documents which records are included and why, which is the first thing a reader should be able to check.
The invariant that catches most bugs
Once you derive per-state rows from a single extract, every row must be traceable to the same source snapshot:
every derived row has the same as_of date as its source extract
sum of per_state counts == count of included nodes in that extract
count of state pages == count of distinct state keys in that extract
If those three do not hold, you either double-counted nodes that sit on a boundary, dropped rows with a missing state key, or mixed two extracts. Assert all three in the build step and fail loudly. It is a boring check and it catches nearly everything.
Boundaries are a data problem, not a geometry problem
Assigning a point to a state sounds like a point-in-polygon call until you hit the edges: water, roads that run along a border, and nodes tagged slightly off-position by a contributor standing on the opposite kerb. Pick one rule, write it down, and apply it identically to every node. Whether you use administrative boundaries or a nearest-centroid fallback matters less than being consistent and saying which one you used.
A point near a border that you assign differently from a competing map is not a bug in either project. It is a different documented rule. That is one reason published totals disagree.
Why totals disagree between projects
If two sites built on the same open dataset show different numbers, the usual causes are, in order:
- Different extract dates. The dataset changes daily as volunteers add entries.
- Different inclusion rules: all surveillance nodes, or only those with the ALPR type tag; all vendors, or one manufacturer.
- Different geography: US only, or worldwide; whether the District of Columbia is its own row.
- Different deduplication, especially where a contributor added both a node and a way.
- Different boundary assignment, as above.
None of these implies someone is wrong. It means "count of documented cameras" is a derived metric with a definition attached. Print the definition next to the number, and print the extract date with it. A count without an as-of date is not a fact you can compare.
The state-level breakdown is an example of putting the rollup next to its own footnote instead of letting the number stand alone.
License and attribution are part of the schema
The dataset is distributed under the Open Database License. If you republish a derived extract, attribution and share-alike obligations travel with it. Put the credit and the license in the page template, not in a changelog nobody reads, and keep a plain link back to the source project.
What I would do differently
On a first pass I treated "unknown manufacturer" as a category to display. It is not a category, it is an absence, and readers consistently misread it as "a different vendor". Same for missing bearings: rendering a cone only for nodes that have one is fine, but the page has to say that cones represent recorded bearings, not an estimate.
The general lesson: every derived field should either carry its provenance or be visibly marked as unknown. In a volunteer dataset, absence of evidence is the most common value you will ever handle.
Further reading
If you want to see these ideas applied to a live public map, the methodology page describes the extract pipeline, and the state rollup shows how the per-state rows are presented.
Top comments (0)