Proximity link types
Declare spatial (and spatio-temporal) relationships between two coordinate-bearing object types — "this aircraft was within 50 km of this military base at the time of observation" — as first-class, governed, queryable link types in your ontology pack.
This page covers proximity link types — a new edge-inference strategy added alongside the existing value-join ("reference") strategy. It extends the Ontologies authoring guide. Read that guide first if you are new to ontology packs.
What is a proximity link?
Standard link types in an ontology pack infer edges by value-join — a source object's property must equal a target object's identity column (FK semantics). This works well for Payment → Counterparty or Aircraft → AircraftType where a shared key exists.
Many real-world relationships are spatial or spatio-temporal, not key-equality:
- An aircraft was near a military base (no shared ID).
- A drone observation was near an aircraft at the same time.
- A submarine cable landing point is near a monitored area.
These cannot be modelled as value-joins. A kind: "proximity" link type infers edges by haversine distance (and optionally a time window), so these relationships become first-class, governed, queryable edges — visible in the instance graph, accessible from the map, chat, and the SDK.
Reference vs proximity at a glance
"reference" (default) | "proximity" | |
|---|---|---|
| Edge inference | Value-join on a key column | Haversine distance (+ optional time window) |
| Required field | — | maxMeters |
| Optional field | — | maxMinutes, six *LatProperty/*LngProperty/*TimeProperty overrides |
| Cardinality | Declared per link | Implicitly N-N (any number of targets in range) |
| Existing packs | Unchanged — kind defaults to "reference" | — |
Declaring a proximity link
Add kind: "proximity" and maxMeters to a defineLinkType(...) entry inside defineOntology({ linkTypes: [...] }):
import { defineOntology, defineLinkType } from '@scrydon/sdk-authoring/ontologies'
export default defineOntology({
id: 'my-osint-pack',
version: '1.0.0',
displayName: 'My OSINT Pack',
description: 'Aircraft, bases, and cable infrastructure.',
maturity: 'preview',
author: { name: 'Acme Intelligence' },
objectTypes: [/* ... Aircraft, MilitaryBase ... */],
linkTypes: [
defineLinkType({
slug: 'AircraftNearBase',
displayName: 'Aircraft near base',
sourceTypes: ['Aircraft'],
targetTypes: ['MilitaryBase'],
kind: 'proximity', // NEW — edge inferred by haversine distance
maxMeters: 50000, // 50 km threshold (required for proximity)
}),
],
bindings: [/* ... */],
})The platform computes edges at query time over the governed projection of Aircraft and MilitaryBase instances. No new read path, no raw DB queries — the same DLP/RLS/clearance rules that apply to map and chat already govern which nodes are loaded.
Schema reference
| Field | Type | Required | Notes |
|---|---|---|---|
slug | Slug | Yes | Unique identifier, e.g. "AircraftNearBase". |
displayName | string | Yes | Human-readable edge label shown on the graph. |
description | string | No | Optional longer description. |
sourceTypes | string[] | Yes | Object Type slugs on the source side. |
targetTypes | string[] | Yes | Object Type slugs on the target side. |
kind | "proximity" | Yes | Must be "proximity" to enable spatial inference. |
maxMeters | number | Yes | Spatial threshold in metres, must be > 0. An edge is only created when haversineDistance(source, target) ≤ maxMeters. |
maxMinutes | number | No | Temporal window in minutes. Applies only when both the source and target instance expose a timestamp. Omit for purely spatial relationships. |
sourceLatProperty | string | No | Override the source latitude column name. |
sourceLngProperty | string | No | Override the source longitude column name. |
sourceTimeProperty | string | No | Override the source timestamp column name. |
targetLatProperty | string | No | Override the target latitude column name. |
targetLngProperty | string | No | Override the target longitude column name. |
targetTimeProperty | string | No | Override the target timestamp column name. |
Coordinate convention
The platform looks for coordinates on each instance's properties using a convention ladder. You only need the six override fields when your object type uses non-standard column names.
Latitude — tried in order: latitude, lat
Longitude — tried in order: longitude, lng, lon
Timestamp — tried in order: observedAt, seenAt, timestamp, time, dateUtc, sentAt; then any property whose name ends in At or time (case-insensitive). If no timestamp is found, the instance is treated as a static point (time window is ignored for that side).
Valid range check: latitude must be in [-90, 90] and longitude in [-180, 180]. Instances with out-of-range or non-numeric coordinate values are skipped silently — they do not produce proximity edges.
When to use coordinate overrides
Use the per-side override fields when a type's coordinate columns don't follow the convention. For example, a SubmarineCable type might store its landing-point coordinates as anchorLatitude/anchorLongitude:
defineLinkType({
slug: 'AircraftNearCable',
displayName: 'Aircraft near cable landing',
sourceTypes: ['Aircraft'],
targetTypes: ['SubmarineCable'],
kind: 'proximity',
maxMeters: 20000,
// Aircraft uses standard lat/lng — no source override needed.
// SubmarineCable uses non-standard column names — override the target side:
targetLatProperty: 'anchorLatitude',
targetLngProperty: 'anchorLongitude',
})The Aircraft side resolves coordinates from latitude/longitude by convention; the SubmarineCable side resolves them from anchorLatitude/anchorLongitude explicitly.
Spatial-only vs spatio-temporal
Spatial-only (no maxMinutes): an edge is created whenever source and target are within maxMeters. The types' timestamp properties are irrelevant.
defineLinkType({
slug: 'BaseNearCable',
displayName: 'Base near cable landing',
sourceTypes: ['MilitaryBase'],
targetTypes: ['SubmarineCable'],
kind: 'proximity',
maxMeters: 30000, // purely spatial — no time window
targetLatProperty: 'anchorLatitude',
targetLngProperty: 'anchorLongitude',
})Spatio-temporal (maxMinutes set): an edge is created when source and target are within maxMeters and both sides expose a timestamp and those timestamps are within maxMinutes of each other. If either side has no resolvable timestamp the time window is silently skipped and the edge is decided on distance alone.
defineLinkType({
slug: 'DroneObservationNearAircraft',
displayName: 'Drone observation near aircraft',
sourceTypes: ['DroneObservation'],
targetTypes: ['Aircraft'],
kind: 'proximity',
maxMeters: 25000,
maxMinutes: 30, // both sides carry timestamps — time window applies
})DroneObservation carries observedAt and Aircraft carries seenAt — both resolve by the convention ladder, so no overrides are needed.
Complete example — AircraftNearBase
A full ontology pack with two object types and one proximity link:
import {
defineOntology,
defineObjectType,
defineProperty,
defineLinkType,
defineIdentityRule,
defineBinding,
} from '@scrydon/sdk-authoring/ontologies'
export default defineOntology({
id: 'military-situational-awareness',
version: '1.0.0',
displayName: 'Military Situational Awareness',
description: 'Aircraft positions and military bases — connected by proximity.',
maturity: 'preview',
author: { name: 'Acme Intelligence Team' },
objectTypes: [
defineObjectType({
slug: 'Aircraft',
displayName: 'Aircraft',
kind: 'analytical',
properties: [
defineProperty({ slug: 'callsign', displayName: 'Callsign', scalarType: 'string', cardinality: 'required' }),
defineProperty({ slug: 'latitude', displayName: 'Latitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'longitude', displayName: 'Longitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'altitude', displayName: 'Altitude (m)', scalarType: 'float' }),
defineProperty({ slug: 'seenAt', displayName: 'Seen at', scalarType: 'timestamp' }),
],
}),
defineObjectType({
slug: 'MilitaryBase',
displayName: 'Military Base',
kind: 'analytical',
properties: [
defineProperty({ slug: 'name', displayName: 'Name', scalarType: 'string', cardinality: 'required' }),
defineProperty({ slug: 'country', displayName: 'Country', scalarType: 'string' }),
defineProperty({ slug: 'latitude', displayName: 'Latitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'longitude', displayName: 'Longitude', scalarType: 'float', cardinality: 'required' }),
],
}),
],
linkTypes: [
defineLinkType({
slug: 'AircraftNearBase',
displayName: 'Aircraft near base',
description: 'Aircraft within 50 km of a military base.',
sourceTypes: ['Aircraft'],
targetTypes: ['MilitaryBase'],
kind: 'proximity',
maxMeters: 50000, // 50 km
// Both types use standard latitude/longitude column names — no overrides needed.
// MilitaryBase has no timestamp — purely spatial.
}),
],
identityRules: [
defineIdentityRule({ objectTypeSlug: 'Aircraft', deterministicKeys: ['callsign'] }),
defineIdentityRule({ objectTypeSlug: 'MilitaryBase', deterministicKeys: ['name'] }),
],
bindings: [
defineBinding({
objectTypeSlug: 'Aircraft',
kind: 'silver_table',
source: { table: 'silver_aircraft_positions' },
identitySpec: { keys: ['callsign'] },
columnMap: {
callsign: 'callsign',
latitude: 'lat',
longitude: 'lon',
altitude: 'altitude_m',
seenAt: 'seen_at',
},
}),
defineBinding({
objectTypeSlug: 'MilitaryBase',
kind: 'silver_table',
source: { table: 'silver_military_bases' },
identitySpec: { keys: ['name'] },
columnMap: {
name: 'base_name',
country: 'country_code',
latitude: 'lat',
longitude: 'lon',
},
}),
],
})Once this pack is installed, the instance graph draws edges labelled "Aircraft near base" between any aircraft and any base within 50 km. Because MilitaryBase has no timestamp property, the relationship is purely spatial — distance is the only criterion.
Graph view radius override
On the Analytics graph canvas you can drag a radius slider under the Display panel to try different thresholds without editing the pack. The slider sends an optional proximityOverride to the instance-graph endpoint that overrides maxMeters (and optionally maxMinutes) for the current view session only — the pack's declared defaults are unchanged.
This lets you answer "show me everything within 200 km" with one drag, rather than re-authoring the pack.
The radius override is global for the current view session — it applies to all proximity links in the ontology at once. Per-link tuning is a planned follow-up.
Troubleshooting
No proximity edges appear
-
Neither side has resolvable coordinates. Check that both the source and target object types have latitude and longitude properties with values in the expected columns. The convention ladder (
latitude/lat,longitude/lng/lon) must match a column that contains a numeric value in[-90, 90]×[-180, 180]. UsetargetLatProperty/targetLngPropertyoverrides when the columns have non-standard names. -
The threshold is too small for the actual data. Try temporarily increasing
maxMeters(or use the radius slider in the graph view) to confirm data is present. A 50 km threshold works for aircraft-to-base; cable infrastructure spread over ocean basins may need 200 km or more. -
Object types are not bound. Proximity inference only runs over instances that are loaded into the graph — which requires the object type to have a working binding. Check that your
defineBinding(...)entries are present and that the upstream silver table contains rows. -
Only one side has coordinates. If the source type has no resolvable coordinates, it contributes no source points and no edges are emitted. Both sides must have coordinates.
Spatio-temporal: time window is not filtering as expected
The time window (maxMinutes) is applied only when both the source and target instance expose a resolvable timestamp. If one side has no timestamp, the time criterion is silently skipped and the edge is decided on distance alone. This is by design — a static object (e.g., a MilitaryBase with no timestamp) is always a valid proximity target regardless of when a source was observed.
If you want strict time-gating on both sides, verify that both types have a timestamp property following the convention (observedAt, seenAt, timestamp, time, dateUtc, sentAt, or any property ending in At/time). Use sourceTimeProperty/targetTimeProperty overrides when the columns have non-standard names.