GDPR for Geospatial Data Lineage

Part of: Regulatory Compliance & Standards Mapping

A coordinate is not automatically anonymous. A latitude and longitude that resolves to a residential parcel, a habitual commuting route, or a single visit to a medical facility is personal data under the General Data Protection Regulation, and once a dataset contains it, every activity that ingests, reprojects, joins, or publishes that data inherits obligations. The problem for geospatial teams is that these obligations are historical: when a data subject invokes their right of access or erasure, you must reconstruct what you did with their location, when, on what lawful basis, and where it flowed — sometimes years after the processing. Lineage is the only mechanism that answers those questions without heroic archaeology, which is why GDPR compliance for location data is fundamentally a provenance-tracking problem rather than a consent-banner problem.

This guide applies GDPR to spatial data lineage concretely: how to detect personal data hiding in coordinates, how to log lawful basis on every activity, how to reconstruct data-subject rights from lineage rows, how to produce Data Protection Impact Assessment evidence, and how to reconcile the right to erasure with an immutable audit trail. It sits within the broader regulatory compliance and standards mapping section, whose control-to-field philosophy drives everything below, and it feeds two companion pages: a precise mapping of GDPR controls to lineage fields and a how-to on anonymizing location data for GDPR.

Reconstructing data-subject rights from the lineage store Subject request (DSAR Art. 15/16/17) Lineage store append-only rows subject_key index lawful_basis + hash Access (Art. 15) assemble history Rectify (Art. 16) append correction Erase (Art. 17) crypto-shred payload Audit trail kept

Prerequisites

Step-by-step

1. Detect personal data in coordinates

Before you can log a lawful basis, you must know which columns are in scope. A coordinate becomes personal data when its precision and context allow it to single out a person. The purpose of this step is to flag processing that touches identifiable location so the pipeline can require a lawful basis downstream. Use resolution and linkage as the test: sub-metre points tied to a subject identifier are almost always personal; a coarse municipal centroid usually is not.

from __future__ import annotations
import geopandas as gpd


def is_personal_location(gdf: gpd.GeoDataFrame, subject_col: str | None) -> bool:
    """Heuristic: identifiable if a subject key exists and points are high-resolution."""
    if subject_col and subject_col in gdf.columns:
        return True
    # Point geometries with sub-block precision are treated as identifiable.
    if (gdf.geometry.geom_type == "Point").all():
        # WGS84 degrees: ~5 decimal places ≈ 1 m; flag anything finer than a block.
        precise = gdf.geometry.apply(lambda p: round(p.x, 4) != round(p.x, 3))
        return bool(precise.any())
    return False

Anything this function flags must carry a lawful basis and, where feasible, be routed through the anonymizing location data for GDPR workflow before wider processing.

2. Log lawful basis on every activity

Article 6 requires that every processing activity rest on a lawful basis, and Article 5 requires purpose limitation. The purpose of this step is to make basis and purpose non-nullable at capture so no personal-location activity can be recorded without them. Write the basis into the same lineage event as the transformation itself.

from __future__ import annotations
from datetime import datetime, timezone
import hashlib
import json
import psycopg


LAWFUL_BASES = {"consent", "contract", "legal_obligation",
                "vital_interests", "public_task", "legitimate_interests"}


def log_activity(conn: psycopg.Connection, *, dataset_id: str, subject_key: str,
                 activity: str, lawful_basis: str, purpose: str) -> str:
    if lawful_basis not in LAWFUL_BASES:
        raise ValueError(f"invalid Art. 6 basis: {lawful_basis}")
    ts = datetime.now(timezone.utc)
    body = json.dumps({"dataset_id": dataset_id, "subject_key": subject_key,
                       "activity": activity, "lawful_basis": lawful_basis,
                       "purpose": purpose, "ts": ts.isoformat()},
                      sort_keys=True, separators=(",", ":"))
    digest = hashlib.sha256(body.encode()).hexdigest()
    conn.execute(
        """INSERT INTO lineage_event
           (dataset_id, subject_key, activity, lawful_basis, purpose,
            valid_from, content_hash)
           VALUES (%s, %s, %s, %s, %s, %s, %s)""",
        (dataset_id, subject_key, activity, lawful_basis, purpose, ts, digest),
    )
    return digest
The identifiability gradient for coordinates Four treatments of the same location data ordered by residual re-identification risk, with a note that population density determines where the line falls. Same source, four treatments — none is automatically "anonymous" Raw GPS trace Personal data, unambiguously Home and workplace inferable from two overnight stops Truncated to 3 decimals Still personal data in most contexts ~110 m cell — one dwelling in rural areas Aggregated to a polygon Depends entirely on population in that polygon A count of 1 is a disclosure with extra steps k-anonymity, k recorded Defensible — because the threshold is asserted and logged Suppress cells below k rather than publishing them

The gradient is the point rather than any individual row: there is no coordinate precision at which data becomes categorically anonymous, because identifiability depends on how many people share the resulting cell. Truncating to three decimals is genuinely anonymising in a dense city block and genuinely identifying on a rural road, and a pipeline applying one rule uniformly will publish disclosures in exactly the places where they are most sensitive.

That is why only the bottom row is defensible: it makes the threshold explicit, tests it against the actual data, and suppresses what fails. Record k, the aggregation unit, and the count of suppressed cells as parameters on the anonymisation activity, so the assessment is auditable rather than assumed. A suppression count of zero across a large heterogeneous area is itself a signal worth checking — it usually means the test was not applied.

3. Reconstruct data-subject rights from lineage

Articles 15 through 17 give subjects the rights of access, rectification, and erasure. The purpose of this step is to resolve any of those requests from the lineage store alone, keyed on subject_key. Because the store is append-only, an access request is a filtered read, and a rectification is a new corrective event rather than an in-place edit.

-- Article 15: assemble the full processing history for one data subject.
SELECT dataset_id, activity, lawful_basis, purpose, valid_from, content_hash
FROM lineage_event
WHERE subject_key = %(subject_key)s
ORDER BY valid_from ASC;

This single query is the backbone of every rights response; the mapping of GDPR controls to lineage fields guide turns its columns into a formal Article 30 record. The append-only design is what makes rectification tractable under Article 16: rather than overwrite a wrong geocode in place, which would destroy the audit trail and leave you unable to prove what the record said before, you append a corrective event that supersedes the earlier one. A point-in-time read reconstructs whichever version was authoritative on a given date by filtering on valid_from, so the store simultaneously honors the subject’s correction and preserves the history a regulator may ask you to defend.

4. Produce DPIA evidence

Article 35 requires a Data Protection Impact Assessment for high-risk processing, and large-scale location tracking usually qualifies. The purpose of this step is to derive DPIA inputs — the categories of data, the purposes, and the actual processing performed — from lineage rather than from a self-reported questionnaire.

-- DPIA input: distinct purposes and bases actually exercised, with volume.
SELECT purpose, lawful_basis, COUNT(*) AS activities,
       MIN(valid_from) AS first_seen, MAX(valid_from) AS last_seen
FROM lineage_event
GROUP BY purpose, lawful_basis
ORDER BY activities DESC;

Grounding the DPIA in observed lineage means the assessment reflects what the system does, not what a form claims it does.

5. Reconcile erasure with an immutable trail

The right to erasure appears to conflict with an append-only audit log. The purpose of this step is to resolve that conflict with crypto-shredding: encrypt each subject’s payload under a per-subject key, and satisfy erasure by destroying the key. The lineage row — its timestamps, activity name, lawful basis, and hash — survives for audit, but the personal payload becomes unrecoverable.

from __future__ import annotations
import psycopg


def erase_subject(conn: psycopg.Connection, subject_key: str) -> int:
    """Crypto-shred: drop the key, retain the tamper-evident metadata."""
    conn.execute("DELETE FROM subject_key_vault WHERE subject_key = %s",
                 (subject_key,))
    # Metadata rows remain; payload ciphertext is now undecryptable.
    cur = conn.execute(
        """UPDATE lineage_event SET payload_erased = TRUE
           WHERE subject_key = %s RETURNING id""", (subject_key,))
    return len(cur.fetchall())

Configuration reference

Parameter Type Valid values Default
subject_key text opaque per-subject identifier (never raw PII) required
lawful_basis enum consent, contract, legal_obligation, vital_interests, public_task, legitimate_interests required
purpose text free text bound to a processing register entry required
retention_days integer 1–3650 730
precision_flag enum identifiable, anonymized, pseudonymized identifiable
payload_encryption enum per_subject_key, shared_key, none per_subject_key
content_hash text SHA-256 hex over the canonical event body auto

Where Erasure and Immutability Actually Meet

Erasing the payload while retaining the record A lineage record points to a payload; the payload is destroyed and an erasure event appended, leaving the audit trail intact and the personal data gone. Lineage record — RETAINED activity, actor, lawful basis timestamp, purpose sha256 of what was processed points to Payload — DESTROYED the coordinates themselves NEW appended event: erasure — request id, date, scope, authorising actor the compliance act is itself a lineage fact Deleting the record instead destroys your only proof that you complied.

The retained hash deserves a moment’s thought, because it looks like a loose end. A SHA-256 of erased personal data is not itself personal data in any practical sense — it is not reversible, and it cannot be used to identify anyone without already possessing the original. What it does provide is the ability to demonstrate, later, that a specific dataset was the one processed under a specific basis, which is precisely the demonstration an Article 30 record exists to support. Where a regulator takes a stricter view for a particular category, the resolution is to record the digest of a salted derivation rather than to omit the field entirely.

Scope is the harder question in practice. An erasure request covers a data subject, while lineage records cover datasets, and the mapping between them is rarely one-to-one. Maintain a subject-to-dataset index built at ingestion rather than attempting to search payloads at request time — reconstructing which of four hundred derived products contains a given subject, months later, is exactly the work the index exists to avoid.

Common failure modes & mitigations

Failure mode Symptom Mitigation
Silent CRS drift on personal points Coordinates shift meters during reprojection, breaking subject matching Log input and output CRS on every transform; validate subject_key joins survive reprojection
Anonymization treated as erasure Truncated geohashes still re-identify via linkage Record anonymization as its own event; measure k-anonymity, do not assume it
Lawful basis backfilled Activities logged without a basis, patched later Enforce NOT NULL on lawful_basis; reject inserts at the pipeline boundary
Erasure breaks the hash chain Deleting rows invalidates downstream content hashes Crypto-shred the payload, never delete the metadata row
Subject key = raw PII The lineage store itself becomes a breach surface Store an opaque token; keep the token-to-identity map in a separate vault

Cross-Border Transfer, Which Coordinates Make Concrete

Transfer rules are usually discussed in terms of where a server sits, and geospatial work adds a second dimension that trips teams up: the data describes a place, and the place is not necessarily where the processing happens. Both matter, and they are governed differently.

The transfer question is about the processing location. Reprojecting an EU residents’ dataset on a worker in another jurisdiction is a transfer regardless of where the coordinates point, and it needs a lawful transfer mechanism recorded alongside the lawful basis. Because pipelines schedule work onto whichever worker is free, this is easy to violate accidentally — a burst that spills into a secondary region turns a compliant nightly job into a transfer nobody authorised.

Record the processing region as a field on every activity, not as a property of the system. A system-level statement that “processing occurs in the EU” is unverifiable after the fact and false the first time capacity spills elsewhere; a per-activity region field makes the exception queryable and lets a scheduled check assert that no activity on a restricted dataset ran outside its permitted set. Pair it with the dataset’s own permitted-region list so the assertion is data-driven rather than hard-coded.

The second dimension — what the coordinates describe — matters for a different reason. Data about locations in a jurisdiction can attract that jurisdiction’s rules even when processed elsewhere, and sensitive-site restrictions in particular are usually tied to the territory depicted rather than the server. Carrying the depicted territory as an attribute of the dataset, derived from its extent at ingestion, is what makes those rules applicable automatically rather than by someone remembering.

Compliance & governance alignment

GDPR article Requirement Lineage field or practice
Art. 5(1)(b) Purpose limitation purpose bound to a processing-register entry
Art. 5(1)(e) Storage limitation retention_days, enforced by scheduled expiry job
Art. 6 Lawful basis for processing non-nullable lawful_basis enum on every event
Art. 15 Right of access subject_key-indexed read of all events
Art. 17 Right to erasure crypto-shred via subject_key_vault key deletion
Art. 30 Records of processing aggregation over purpose, lawful_basis, recipients, transfers
Art. 35 Data Protection Impact Assessment derived from observed purpose/lawful_basis distribution
Art. 44 Cross-border transfer records transfer_destination and safeguard fields on export events

These mappings connect directly to the field-level crosswalk in the control-to-lineage-field mapping guide and to the anonymization practice in the anonymizing location data for GDPR how-to. Treated together, they let a data protection officer answer any subject request or supervisory-authority query from the lineage store itself, which is the entire point of building compliance in at the field level rather than bolting it on at audit time.

Frequently Asked Questions

Is a coordinate personal data?

It depends on what it can be linked to, which is the answer the regulation itself gives. A building footprint is not; the same footprint labelled as one person’s residence is. A GPS trace is, almost always, because movement patterns identify individuals even without a name attached. Treat the question as an assessment recorded per dataset rather than a property of the geometry type.

Can we rely on aggregation to take data out of scope?

Only with a recorded threshold test. Aggregation reduces identifiability by an amount that depends on the underlying population, so the same aggregation is anonymising in one polygon and disclosing in another. Apply a k threshold, suppress cells below it, and log the parameters — an aggregation without a threshold is a hope, not a control.

How do we handle a subject-access request for derived products?

Answer from the derivation graph rather than from the datasets. Start at the subject-to-dataset index, walk downstream edges, and enumerate every product that inherited the data. This is the query the graph exists to serve, and it is also the check that reveals products nobody remembered were derived from personal sources.

Does GDPR apply to lineage records themselves?

Yes, where they contain personal data — and actor fields routinely do. Staff names in actor are personal data about employees, with their own lawful basis and retention considerations. Use role or service-account identifiers in the record and resolve to individuals through a separately governed roster.

What lawful basis applies to public-sector mapping?

Usually public task rather than consent, which materially changes the obligations: there is no right to erasure under public task in the same form, but there is a right to object that must be considered. Recording the basis explicitly per activity — rather than assuming one basis for the whole system — is what makes those differences applicable at the granularity they actually apply.

How long can we keep lineage on erased data?

As long as the accountability obligation lasts, which is typically longer than the data itself. The retained record is what demonstrates lawful processing and lawful erasure; discarding it early leaves you unable to answer the question the retention was for. Set its schedule from the accountability clock rather than from the dataset’s.