Capturing Lineage for AWS Location Service Operations
Geocoding an address, snapping a GPS trace to roads, or calculating a route through AWS Location Service each derives new spatial data from a managed provider, and when that output flows into a downstream dataset you need a record of which operation produced it and against which resource. This how-to wraps boto3 Location Service calls so every place, route, and map request emits a JSON lineage document. It sits under Structuring JSON/XML Lineage Documents, and the emitted documents are designed to land in a write-once archive such as Object Storage WORM Retention.
Prerequisites
- Python 3.10+ and
boto31.34+. - An IAM principal with
geo:SearchPlaceIndexForText,geo:CalculateRoute, and relatedgeo:*permissions for the resources you call. - Existing AWS Location resources: a Place Index and/or a Route Calculator, created in the same region as your client.
- AWS credentials via
AWS_PROFILE, environment variables, or an instance role. SetAWS_REGIONto match your Location resources.
Implementation
The wrapper records the operation name, the target Location resource, a hashed request payload (so the same query is deduplicated and no raw address is stored in the clear), a summary of the response geometry, and the AWS request id from the response metadata. That request id is the anchor that ties your lineage record back to CloudTrail.
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import boto3
def geocode_with_lineage(
place_index_name: str,
text: str,
lineage_dir: str | Path,
max_results: int = 1,
) -> dict[str, Any]:
"""Geocode text via AWS Location and emit a JSON lineage document.
Args:
place_index_name: Name of the Location Service Place Index resource.
text: The address or place text to geocode.
lineage_dir: Directory that receives the .json lineage document.
max_results: Maximum number of candidate results to request.
Returns:
The lineage document written to disk.
"""
out_dir = Path(lineage_dir)
out_dir.mkdir(parents=True, exist_ok=True)
client = boto3.client("location")
requested_at = datetime.now(timezone.utc)
response = client.search_place_index_for_text(
IndexName=place_index_name,
Text=text,
MaxResults=max_results,
)
# Never persist the raw query text; hash it so records stay privacy-safe.
query_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()
results = response.get("Results", [])
positions: list[list[float]] = [
r["Place"]["Geometry"]["Point"]
for r in results
if r.get("Place", {}).get("Geometry", {}).get("Point")
]
summary = response.get("Summary", {})
lineage: dict[str, Any] = {
"event": "location_geocode",
"operation": "SearchPlaceIndexForText",
"resource": place_index_name,
"data_source": summary.get("DataSource", "UNKNOWN"),
"query_sha256": query_hash,
"result_count": len(results),
"positions": positions, # [lon, lat] pairs, EPSG:4326
"crs": "EPSG:4326",
"aws_request_id": response["ResponseMetadata"]["RequestId"],
"requested_at": requested_at.isoformat(),
}
record_name = f"{lineage['operation']}_{query_hash[:16]}.json"
(out_dir / record_name).write_text(json.dumps(lineage, indent=2), encoding="utf-8")
return lineage
if __name__ == "__main__":
doc = geocode_with_lineage(
place_index_name="agency-places",
text="1600 Pennsylvania Ave NW, Washington, DC",
lineage_dir="./lineage",
)
print("Captured", doc["operation"], "from", doc["data_source"])
The data_source field from the response Summary (for example Esri or Here) is essential provenance: AWS Location proxies third-party providers whose licensing and accuracy differ, so a record that omits the provider cannot support a downstream accuracy claim.
Verification
Cross-reference the captured aws_request_id against CloudTrail to prove the call happened as recorded:
import boto3
ct = boto3.client("cloudtrail")
events = ct.lookup_events(
LookupAttributes=[
{"AttributeKey": "EventName", "AttributeValue": "SearchPlaceIndexForText"}
],
MaxResults=5,
)
for e in events["Events"]:
print(e["EventTime"], e["EventId"])
The event time in CloudTrail should fall within seconds of the requested_at timestamp in your JSON document, and the operation name must match. A lineage record whose request id has no CloudTrail counterpart indicates the call was mocked or replayed and should not be trusted as evidence.
Gotchas & edge cases
- Coordinate order is
[longitude, latitude]. AWS Location returns and expects GeoJSON-style[lon, lat]positions, the reverse of thelat, lonorder humans write. Store the order explicitly, as above, or a downstream consumer that assumes[lat, lon]will place every point in the wrong hemisphere. - Route geometry can be large and unstable.
CalculateRoutewithIncludeLegGeometryreturns a dense polyline that differs run-to-run as the provider updates its road graph. Hash a normalized summary (distance, duration, waypoint order) rather than the full geometry, or identical routing intents will appear as distinct lineage events. - Records must be immutable to count as evidence. A lineage document that can be edited after the fact proves nothing. Write each document once and push it to a locked store as described in Object Storage WORM Retention, so the provider, request id, and geometry summary cannot be altered after capture. Keep the schema aligned with Structuring JSON/XML Lineage Documents so Location events query the same way as every other source.