Regulatory Obligations for Location Data
Location data becomes personal data the moment a row can be linked to an identifiable person, and spatial joins make that linkage far easier than the raw columns suggest — which is why the assessment has to be an engineering step in the publication pipeline rather than a legal opinion obtained once. This topic sits under Spatial Data Audit Reporting & Compliance Governance and covers the obligations, the assessment, and the artifacts that demonstrate both were considered. It describes engineering practice rather than legal advice; the thresholds that matter in a given jurisdiction are a question for counsel.
Prerequisites
- An inventory that records, per dataset, whether it contains location data about individuals. Almost no catalogue has this field, and nothing else here is possible without it.
- Python 3.11+ with
geopandas==0.14.4andshapely==2.0.4for the spatial reasoning the assessment needs. - A stated purpose per published dataset. Proportionality is judged against a purpose, so a dataset published because it exists cannot be assessed at all.
- A place to record the assessment. A negative finding that was never written down cannot be produced later, which is the situation this whole area is designed to avoid.
Concept & Spec Reference
Four categories cover most of what a spatial team encounters, and they are worth separating because they carry different obligations and different engineering responses.
| Category | Example | Why it is sensitive | Usual mitigation |
|---|---|---|---|
| Direct identifiers with location | Address points with occupant names | Identifies a person directly | Remove the identifier; publish the point only if the address itself is public |
| Indirectly identifying points | Incident locations, service call points | Address-level points resolve to households | Aggregate to a zone, or displace within a radius |
| Trajectories | Vehicle traces, mobility records | Origin and destination pairs identify home and work | Trim endpoints, aggregate to flows |
| Small-count aggregates | Counts by output area | A count of one identifies its member | Suppress below a threshold, or widen the zone |
The fourth row is the one most often missed, because aggregation is treated as the answer rather than as a step with its own failure mode. A table of counts per small area with a value of one in a rare category identifies an individual as effectively as a point, and publishing two such tables that differ by one record can reveal that record entirely. Threshold suppression exists for this reason and is not optional.
Three properties of spatial data make this domain behave differently from tabular privacy work.
Location is quasi-identifying at a resolution people underestimate. A home location at street level, combined with a workplace at street level, is close to unique in most populations. Neither column is an identifier; the pair is.
Precision is often fabricated. A dataset stored to nine decimal places implies a positional certainty the collection method never had. Publishing at that precision is both misleading and a privacy decision made by accident, which is one reason enforcing coordinate precision and CRS in CI belongs in the pipeline.
Aggregation is reversible more often than expected. Differencing two releases of the same aggregate, or intersecting an aggregate with a boundary it does not respect, recovers detail that the aggregation was intended to remove.
Implementation Walkthrough
Step 1 — Classify the dataset
The classification is a small function and its value is that it runs on everything rather than on the datasets somebody remembered to ask about.
"""Classify a dataset's exposure to location-privacy obligations."""
DIRECT_IDENTIFIER_HINTS = {
"name", "surname", "forename", "email", "phone", "nhs", "ni_number",
"account", "customer", "occupant", "resident",
}
INDIRECT_HINTS = {"dob", "date_of_birth", "postcode", "household", "uprn"}
def classify(columns, geometry_type, has_person_rows):
"""Return a category and the columns that triggered it."""
lowered = {c.lower(): c for c in columns}
direct = [lowered[c] for c in lowered if any(h in c for h in DIRECT_IDENTIFIER_HINTS)]
indirect = [lowered[c] for c in lowered if any(h in c for h in INDIRECT_HINTS)]
if direct:
return {"category": "direct", "triggers": direct}
if has_person_rows and geometry_type in {"Point", "MultiPoint"}:
return {"category": "indirect_points", "triggers": indirect or ["point per person"]}
if has_person_rows and geometry_type in {"LineString", "MultiLineString"}:
return {"category": "trajectory", "triggers": ["path per person"]}
if has_person_rows:
return {"category": "aggregate", "triggers": indirect}
return {"category": "not_personal", "triggers": []}
Step 2 — Assess proportionality against the stated purpose
def assess(category, resolution_m, purpose_resolution_m, min_count=None, threshold=5):
"""Findings, each naming a concrete mitigation."""
findings = []
if category == "direct":
findings.append("remove direct identifiers before publication")
if category in {"indirect_points", "trajectory"} and resolution_m < purpose_resolution_m:
findings.append(
f"published at {resolution_m} m where the purpose needs "
f"{purpose_resolution_m} m; generalise")
if category == "trajectory":
findings.append("trim the first and last segments; endpoints identify home and work")
if category == "aggregate" and min_count is not None and min_count < threshold:
findings.append(
f"smallest cell count is {min_count}, below the suppression threshold "
f"of {threshold}")
return findings
The comparison against purpose_resolution_m is what turns a subjective judgement into a check. A dataset published for route planning needs street-level geometry; one published to show service coverage does not, and the difference is stateable in metres before anyone argues about it.
Step 3 — Record the decision
import datetime
import json
def record(dataset_id, classification, findings, decided_by, mitigations_applied):
"""The artifact that answers a question asked years later."""
return json.dumps({
"dataset": dataset_id,
"assessed_on": datetime.date.today().isoformat(),
"assessed_by": decided_by,
"category": classification["category"],
"triggers": classification["triggers"],
"findings": findings,
"mitigations_applied": mitigations_applied,
"outcome": "published" if not findings or mitigations_applied else "withheld",
}, indent=2, sort_keys=True)
Validation & CI Integration
def test_direct_identifiers_are_always_flagged():
result = classify(["occupant_name", "geom"], "Point", True)
assert result["category"] == "direct"
def test_negative_assessment_still_produces_a_record():
classification = classify(["road_id", "geom"], "LineString", False)
document = record("roads", classification, [], "gis-team", [])
assert '"category": "not_personal"' in document
def test_small_cell_counts_are_reported():
findings = assess("aggregate", 1000, 1000, min_count=2, threshold=5)
assert findings and "suppression threshold" in findings[0]
The second test encodes the discipline that makes the rest worthwhile. A recorded negative assessment — what was checked, on which columns, at what resolution, by whom — is the artifact that answers a regulator’s or an auditor’s question, and it costs nothing because the same function produces it.
Run the classification over the whole catalogue on a schedule rather than gating on it. Route findings to the dataset’s owner as described in compliance dashboards for spatial catalogs, and treat an unclassified dataset as the highest-priority finding, since it is the only state in which nobody has looked.
Derivative & Lineage Management
A privacy assessment describes a dataset as it stood, and three ordinary pipeline events invalidate it.
A join adds columns. Combining an assessed dataset with another can reintroduce identifiability that neither had alone — the classic case being a de-identified point layer joined to a small-area table that makes the points uniquely attributable. Assessments should be attached to products, not only to sources, and re-run at the join.
A release is repeated. Publishing an aggregate monthly creates a differencing opportunity that no single release has. This is a property of the series rather than of any dataset in it, and it is invisible to a per-dataset assessment.
The purpose changes. A dataset released for internal analysis and later added to an open portal has not changed, and the proportionality judgement made about it no longer applies. Recording the intent alongside the assessment is what makes the mismatch detectable, exactly as it is for licensing.
Attaching the assessment to the lineage graph described in spatial data lineage and provenance tracking makes the first case queryable: given a published product, the set of assessments covering its inputs is a traversal rather than a search.
Data Subject Requests Against Spatial Holdings
An access or erasure request is where a location-data programme discovers whether its records are organised around datasets or around people, and spatial holdings are almost always organised around datasets.
The request itself is simple to state: does this organisation hold location data about me, and if so, what. Answering it requires finding the individual across an estate of layers, which raises three problems that are worth solving before a request arrives rather than during the thirty days it allows.
Search is spatial, not textual. A person’s identifier will not appear in a de-identified point layer. What may be there is a point at their address, a trajectory whose endpoint is their home, or a small-count cell containing only them. Locating those requires a spatial query from a known location outward, which means the responder needs the address — and needs to be careful, because searching for a person by location in a de-identified dataset is itself a re-identification attempt and should be logged as one.
Erasure interacts badly with aggregates. Removing one record from a published aggregate changes the counts, and publishing the corrected version alongside the original is precisely the differencing attack the suppression threshold exists to prevent. The workable responses are to suppress the affected cells in both versions or to withdraw the release entirely, and both need deciding before the request rather than under time pressure.
Derived products multiply the work. A record erased from a source layer persists in every product built from it, and finding those products is a lineage query. Where the lineage graph described in spatial data lineage and provenance tracking exists, this is a traversal taking seconds; where it does not, it is an investigation whose completeness nobody can attest to.
The preparation worth doing in advance is narrow and cheap: know which datasets are in scope by keeping the classification current, know what derives from what, and have a written procedure for the aggregate case. A programme with those three can answer a request; one without them will spend the response window discovering it cannot.
Retention Is an Obligation, Not a Preference
Location data about individuals carries a retention limit tied to its purpose, which is a different rule from the one governing evidence and audit artifacts. The two are frequently conflated, with the result that personal location data is retained on the seven-year schedule written for compliance evidence.
Separating them requires only that the retention policy be keyed on data class rather than on storage location. A trajectory dataset collected for a six-month study and an audit verdict about that dataset have different lifetimes, and holding both under the same rule means one of them is wrong. Where a dataset genuinely must be kept beyond its original purpose — for a statutory archive, say — that is a separate lawful basis and deserves recording as such rather than being inherited by default from the retention schedule of the system it happens to sit in.
Who Decides, and on What Evidence
The assessment produces findings; somebody still has to decide. Leaving that implicit is how a dataset is published because nobody said not to.
Three roles cover it without inventing a committee. The dataset owner states the purpose and the resolution it requires, because only they know what the data is for. The engineer runs the classification and the measurements and reports what the data actually contains, which is frequently not what the owner believed. And a named approver — one person, not a function — records the decision to publish, mitigate or withhold.
What makes this work is that the approver decides on evidence rather than on a summary. A record stating that the smallest cell count is two, that the points sit at address-level resolution, and that the stated purpose needs 500 metres, supports a decision anyone can review. A record stating that the dataset was assessed and found acceptable supports nothing, and is what most catalogues contain.
Pitfalls & Resolution Table
| Pitfall | Root Cause | Resolution Strategy |
|---|---|---|
| De-identified points re-identified by a spatial join | Assessment considered columns in isolation | Assess the product after joins, not only the sources |
| Aggregate release reveals individuals through differencing | Each release assessed alone; the series never was | Fix the geography and the suppression rule across releases; assess the series |
| Coordinates published at fabricated precision | Export defaults carried through from the capture format | Round at export to the precision the accuracy justifies |
| Displacement applied per release with a new random seed | Re-running the pipeline moves points differently each time | Derive the displacement deterministically from a stable per-feature key |
| Trajectory endpoints identify home and work | Only the identifier columns were removed | Trim terminal segments, or publish flows rather than paths |
| Assessment exists but cannot be found | Recorded in a document store separate from the catalogue | Store the assessment with the dataset record and stamp its date |
| Nobody assessed the dataset at all | No inventory field says whether it concerns people | Treat unclassified as the highest-priority finding, above any failure |
Related
- Assessing Re-identification Risk in Point Datasets — the k-anonymity style measurement behind the point rows above
- Redacting Sensitive Locations Before Publication — deterministic displacement, suppression and aggregation
- Documenting Cross-Border Transfer of Spatial Data — the record a transfer needs
- Spatial Data Lineage & Provenance Tracking — attaching assessments to products rather than sources
- Spatial Data Audit Reporting & Compliance Governance — the parent guide to evidence, verdicts and reporting