--- title: "Creating Persistent Spatial Identifiers" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Creating Persistent Spatial Identifiers} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.5 ) ``` ## The problem Longitudinal polygon datasets often lack identifiers that remain meaningful when boundaries or labels change. `spatpersist` creates those identifiers from spatial continuity. It does not require prior IDs and does not use names to determine identity. This vignette uses a synthetic dataset containing four common cases: - a stable polygon; - a polygon with an expanded boundary; - an unchanged polygon with a new name; - one polygon that splits into two successors. ```{r example-data} library(spatpersist) polygons <- example_units() sf::st_drop_geometry(polygons) ``` ## Create IDs The default rule continues an identity when at least 75 percent of the earlier polygon is preserved. Candidate links are selected one-to-one. ```{r create} result <- persist_ids( polygons, time = "year", threshold = 0.75, metric = "share_old" ) sf::st_drop_geometry(result)[ , c( "year", "name", "spatial_id", "spatial_version_id", "lineage_id", "parent_id", "transition_type" ) ] ``` The output separates four concepts: 1. `spatial_id` identifies one continuing unit. 2. `spatial_version_id` identifies consecutive boundary spells within that unit. 3. `lineage_id` connects related units through splits, mergers, or replacements. 4. `parent_id` records the strongest predecessor when a related observation starts a new identity. The renamed polygon retains its identity because its geometry is unchanged. The expanded polygon retains its identity but advances to a new geometry version. Under the default rule, neither half of the split preserves 75 percent of the old polygon, so both receive new identities while remaining in the old polygon's lineage. ## Matching rules and ambiguity Three spatial metrics are available: - `share_old`: intersection divided by predecessor area; - `share_new`: intersection divided by successor area; - `iou`: intersection divided by union area. `match_rule = "greedy"` ranks all eligible links and selects them one-to-one. `match_rule = "mutual_best"` considers only links that are best for both sides. Near ties are controlled separately: ```{r conservative-matching} conservative <- persist_ids( polygons, time = "year", metric = "iou", threshold = 0.75, match_rule = "mutual_best", ambiguity_tolerance = 0.05, ambiguity_action = "new" ) ``` The three ambiguity actions are: - `"flag"`: select deterministically and set `match_ambiguous = TRUE`; - `"new"`: reject near-tied links and start new identities; - `"error"`: stop so the user can inspect the evidence. `match_confidence` combines the selected spatial score with its separation from eligible competitors. A score near zero indicates a tie or a candidate that lost a stronger competing link. ## Audit transitions Automatic identity decisions should remain inspectable. The transition table contains every positive-area overlap, not just selected links. ```{r diagnostics} transitions <- id_transitions(result) transitions[ , c( "old_spatial_id", "new_spatial_id", "share_old", "share_new", "iou", "eligible", "mutual_best", "ambiguous", "selected", "lineage_link" ) ] ``` An identity-level summary is also available: ```{r summaries} id_lineages(result, time = "year") ``` ## Visualize splits and mergers ```{r plot-lineage} plot_lineage( result, time = "year", lineage_id = "LID000004" ) ``` Solid edges are selected identity continuations. Dashed edges are broader lineage links. In the example, the two dashed branches show the split without forcing either child to reuse the predecessor's identity. ## Validate the result ```{r validate} issues <- validate_ids(result, time = "year") issues ``` A zero-row issue table means the result passed all implemented checks. These include within-period uniqueness, lineage consistency, sequential geometry versions, parent availability, transition labels, and match diagnostics. Validation confirms structural consistency; it does not prove that a chosen spatial continuity rule is substantively correct for a particular research design. Thresholds and diagnostics should still be reviewed. ## Calibrate before production Matching parameters encode a research decision and should be calibrated before IDs are frozen. A practical calibration workflow is: 1. Transform geographic coordinates to an appropriate equal-area projected CRS and choose a precision suitable for the source maps. 2. Define the relative cost of a false continuation and a false new identity. 3. Compare a small grid of metrics, thresholds, and matching rules against any trusted links or independently reviewed transitions. 4. Inspect disagreement cases and selected links with low confidence or `match_ambiguous = TRUE`. 5. Set `max_time_gap` explicitly when continuity should not cross missing periods. 6. Record the chosen parameters and preserve the accepted output as a registry. Agreement with an existing identifier system is evidence, not proof. A reference system may use names, institutional knowledge, or other attributes that `spatpersist` intentionally does not use. Persistent disagreements should therefore be interpreted substantively rather than resolved by maximizing a single aggregate score. ## Preserve IDs across reruns Passing a prior result as `registry` protects issued IDs when rows are reordered or new units are added. ```{r registry} rerun <- persist_ids( polygons, time = "year", registry = result ) all(rerun$registry_matched) identical(rerun$spatial_id, result$spatial_id) ``` Registry recovery uses same-time geometry and defaults to an IoU of 0.999999. Conflicting identity or geometry-version anchors stop with an error. New identities are numbered above the registry's largest existing spatial ID. If new spatial evidence joins observations anchored to multiple existing lineages, the joined component deliberately consolidates to the lexicographically smallest existing `lineage_id`. All generated identifiers are dataset-local. Separate projects independently start with identifiers such as `SID000001` and `LID000001`, so combine outputs only with an additional project or dataset key. Persistence is scoped to one dataset and its registry chain, not to a global namespace. ## Geometry and time controls By default, invalid geometries stop processing. They can be repaired explicitly: ```r result <- persist_ids( polygons, time = "year", geometry_action = "repair" ) ``` `geometry_precision` sets an `sf` precision scale before spatial operations. For example, `geometry_precision = 100` rounds coordinates to two decimal places when geometries are passed to spatial libraries. After repair and precision are applied, exact coextensive geometries within one time period are rejected. Because attributes are intentionally excluded from matching, no reproducible spatial tie-breaker exists for those observations. `max_time_gap` prevents continuity across large gaps in numeric or date-based time columns. Its units are the time column's numeric units, days for `Date`, and seconds for `POSIXt`. ## Interpretation limits - Names and descriptive fields do not determine identity. - Matching is one-to-one even though lineage relationships may be many-to-many. - `parent_id` stores one strongest predecessor; the transition table contains complete merger evidence. - Registry IDs use the package formats `SID000001` and `SID000001-V001`. - Thresholds encode research decisions and should be reported with results.