# VECTORS ADS-B method specification

**Offered under:** ODbL 1.0 §4.6(b), for the ADSB.lol-derived component of VECTORS.

**Processing version covered:**

| Identifier | Value |
| --- | --- |
| Geometry version key | `g2.p1.r2.l1.f1` |
| Geometry processing (`g`) | 2 |
| Phase logic (`p`) | 1 |
| Runway inference (`r`) | 2 |
| LOD parameters (`l`) | 1 |
| Geometry artifact format, major (`f`) | 1 |
| Corpus processing | 1 |
| ADS-B trace parser | 1 |
| Point simplification | 1 |
| Quality scoring | 1 |
| Airport local-frame contract | 1 |

Outputs record the identifiers they were made with. Additional Contents: `additional-contents.md`. Earlier versions: `CHANGES.md`.

---

## 0. Scope

### 0.1 The source database

ADSB.lol historical data (`adsblol/globe_history_YYYY`), ODbL 1.0: one `trace_full_<icao24>.json` per aircraft address per UTC day, readsb trace format.

### 0.2 The derivative component this document describes

The component has three layers:

1. **Daily corpus.** Every retained leg of each processed day, with:
   - its derived attributes (§3 to §6);
   - its simplified observed points (§7).
2. **Physical-flight records.** Selected corpus legs, with derived attributes (§9).
3. **Flight geometry.** For each physical-flight record:
   - a master geometry, with its phases, runway inference, contacts, connectors and stored reconciliations;
   - its level-of-detail (LOD) subsets and a stored summary (§10 to §12).

### 0.3 What is not part of this component

Outside the component and not offered:

- customer flight records, imports, Calendar, email and other user data;
- how legs are matched or associated with customer flights, and flight identity resolution;
- Flightradar24 data;
- U.S. DOT BTS operating identity;
- airport-surface data (FAA, OurAirports, Natural Earth);
- rendering and application code.

### 0.4 Conventions

- **Time:** Unix seconds, fractional where the source is fractional.
- **Altitude:**
  - "barometric altitude" is trace element 3, exactly as received: a number in feet, the literal `"ground"`, or null;
  - "geometric altitude" is trace element 10;
  - "numeric altitude" means barometric altitude is a finite number.
- **Distance (corpus steps, §2 to §7):** haversine, with mean Earth radius 6,371.0088 km. In §6 only, kilometres are converted to nautical miles by dividing by 1.852.
- **Distance (geometry steps, §10 to §11):** haversine, with mean Earth radius 6,371,008.8 m.
- **Great-circle interpolation:** at fraction `t` between A and B:
  1. Convert both points to unit vectors.
  2. Take the spherical linear interpolation (slerp) `sin((1−t)θ)/sin θ · A + sin(tθ)/sin θ · B`, where θ is the angle between them.
  3. Normalise the result and convert it back to latitude and longitude.

  If θ is below 1e-9 rad (corpus) or 1e-12 rad (geometry), the result is A.
- **Interpolation fraction:** time-based. `t = (t_point − t_A) / (t_B − t_A)`.
- **Bearings:** true degrees in [0, 360). For an undirected axis, the difference between two bearings is folded into [0, 90].
- **Unit conversions:** 1 ft = 0.3048 m; 1 kt = 0.514444 m/s.
- **Flags:** "stale" means bit 0 (value 1) of trace element 6 is set.

### 0.5 Format re-casting (named only)

Decompression, storage, encoding and deletion of the raw archive alter no value.

---

## 1. Input

One ADSB.lol `globe_history` daily release (the repository's preferred release) for UTC day `D`, read in full. Days before 2023-02-16 are not processed.

---

## 2. Trace parsing (parser version 1)

### 2.1 Files

Only `trace_full_<icao24>` files are read; each is one JSON object.

### 2.2 Trace-level fields

| Field | Use |
| --- | --- |
| `icao` | Exactly 6 hex digits, else the trace is skipped. Lower-cased on output. |
| `timestamp` | Base time |
| `trace` | The point array |
| `r` | Registration. Kept as received after trimming whitespace. Null if empty or longer than 32 characters. |
| `t` | Aircraft type designator. Same rule as `r`. |

Every other trace-level field is discarded.

`r` and `t` (from the Mictronics aircraft database, inside the source) are carried as received.

### 2.3 Point fields

Elements of each `trace` point are taken by index:

| Index | Kept as | Rule |
| --- | --- | --- |
| 0 | time offset | `at = timestamp + element 0` |
| 1 | latitude | Must be finite with \|value\| ≤ 90, or the point is dropped |
| 2 | longitude | Must be finite with \|value\| ≤ 180, or the point is dropped |
| 3 | barometric altitude | `"ground"`, a finite number, or null for anything else |
| 4 | ground speed (kt) | Finite number, else null |
| 5 | track (deg) | Finite number, else null |
| 6 | flags | Finite number, else 0 |
| 7 | vertical rate (ft/min) | Finite number, else null |
| 8 | (detail object) | Only `flight`: trimmed, null if empty or longer than 32 characters, then upper-cased |
| 10 | geometric altitude | Finite number, else null |

- Every other element is discarded: element 9, element 11 onward, and every field of the detail object except `flight`.
- Values are not rounded, corrected or converted.

### 2.4 Point-level rules, applied in trace order

1. A point with an invalid position (§2.3) is dropped.
2. **Outside the day.** If `at` is before 00:00:00 UTC of `D`, or at or after 24:00:00 UTC, the point is dropped. An open leg is closed first (§3).
3. **Not increasing.** If the open leg's last point has `at ≥` this point's `at`, the point is dropped.
4. **Callsign carry-forward.** Each kept point carries the trace's current callsign: the most recent non-null `flight` among points kept so far in the trace, including this one (dropped points excluded; not reset between legs).

---

## 3. Leg segmentation (per aircraft, per UTC day)

Per trace (one aircraft, one UTC day), points are added to an open leg in order.

### 3.1 State

Each open leg has:

- `airborne`: false at the start of the leg;
- `groundSince`: the time of the first point in the current unbroken run of `"ground"` altitude, or null.

### 3.2 Boundary rules

A new point `p` follows the open leg's last point `q`. The open leg is closed (§4), and `p` starts a new leg, if **any** of these is true:

1. `p.at − q.at > 1,200 s`.
2. Bit 1 (value 2) of `p`'s flags is set.
3. `p` has a non-null `flight` value that differs from the trace's current callsign, and the current callsign is non-null.
4. `airborne` is true, `groundSince` is not null, and `p.at − groundSince > 300 s`.
5. None of rules 1 to 4 applied, and `distance(q, p) / (p.at − q.at)` exceeds **1,500 km/h**.

After the boundary decision, the trace's current callsign becomes `p`'s `flight` value if that value is non-null.

### 3.3 State update after adding p

- **Ground point.** If `p`'s altitude is `"ground"` and `groundSince` is null, set `groundSince = p.at`.
- **Any other point.** Otherwise set `groundSince = null`. Then, if `p`'s ground speed is a finite number of at least **70 kt** and its altitude is numeric, set `airborne = true`.

At the end of the trace the open leg is closed.

---

## 4. Leg retention (discard rules)

A closed leg is **kept** only if all three hold:

- it has at least **5** points;
- its last point is at least **120 s** after its first;
- `airborne` became true during it (§3.3).

Every other leg is discarded with its points.

---

## 5. Leg attributes

For each kept leg, with first point `F` and last point `L`, these values are derived (everything else is carried as received):

| Attribute | Rule |
| --- | --- |
| `icao24` | The trace address, lower-cased |
| `callsigns` | Sorted list of the distinct non-null point callsigns in the leg |
| `callsign` | The only element of `callsigns` if there is exactly one, else null |
| `registration`, `aircraftType` | `r` and `t` (§2.2), as received |
| `observedStartAt`, `observedEndAt` | `F.at` and `L.at`, as whole seconds (floor) |
| `observedOrigin` | Endpoint rule (§5.1) applied to `F` |
| `observedDestination` | Endpoint rule (§5.1) applied to `L` |

### 5.1 Observed endpoint inference (the 3 km ground-point rule)

- **Airport set:** every row of the OurAirports-derived airport reference (`additional-contents.md` §1) that has:
  - a non-empty latitude and longitude, both finite;
  - an IATA code matching `^[A-Z0-9]{3}$`.
- **The point must be on the surface:** its barometric altitude is `"ground"` and it is not stale. Otherwise the endpoint is null.
- **Candidates:** airports within haversine distance ≤ **3 km**.
- **Result:**
  - exactly one candidate gives that airport's IATA code;
  - zero candidates, or two or more, give null.

---

## 6. Quality classification and scoring (quality version 1)

### 6.1 Measured factors

Factors are measured on the leg's **unsimplified** points, with gap threshold `G = 120 s`. For consecutive points, `step` is the time difference.

| Factor | Definition |
| --- | --- |
| `durationSeconds` | `L.at − F.at` |
| `pointCount` | Number of points |
| `medianIntervalSeconds` | Median of all steps (mean of the two middle values when the count is even). 0 if there are no steps. |
| `maxGapSeconds` | Largest step |
| `coverageRatio` | Sum of steps with `step ≤ G`, divided by `durationSeconds`, capped at 1. 0 if the duration is 0. |
| `maxAltitudeFeet` | Largest altitude, with `"ground"` counted as 0. Null altitudes are ignored. Null if there are none. |
| `altitudeCoverage` | Share of points whose altitude is numeric or `"ground"` |
| `climbObserved` / `descentObserved` | Some pair of consecutive altitude-bearing points rises, or falls, by **≥ 1,000 ft**. Ground counts as 0; null points are skipped. |
| `groundStart` / `groundEnd` | `F` or `L` has altitude `"ground"` |
| `observedOrigin` / `observedDestination` | The §5.1 result is not null |
| `truncatedStart` | `F.at − (start of D) ≤ 90 s` |
| `truncatedEnd` | `(start of D + 86,400) − L.at ≤ 90 s` |
| `implausibleSteps` | Number of steps with step > 0 whose implied speed (haversine km ÷ 1.852 ÷ hours) exceeds **700 kt** |

### 6.2 Score

```
continuity  = 30 · coverageRatio · clamp(durationSeconds / 600, 0, 1)
duration    = 15 · clamp(durationSeconds / 3600, 0, 1)
density     = 15 · (medianInterval ≤ 0 ? 0 : clamp((120 − min(120, medianInterval)) / 105, 0, 1))
structure   = (climbObserved ? 0.35 : 0) + (descentObserved ? 0.35 : 0)
            + (maxAltitudeFeet ≥ 10,000 ? 0.30 : 0)
airborne    = 20 · min(1, altitudeCoverage) · structure
endpoints   = 20 · ( (observedOrigin ? 0.4 : 0) + (observedDestination ? 0.4 : 0)
                   + (groundStart ? 0.1 : 0) + (groundEnd ? 0.1 : 0) )
truncation  = 10 · ( (truncatedStart ? 0.5 : 0) + (truncatedEnd ? 0.5 : 0) )
implausible = pointCount > 1 ? 15 · min(1, implausibleSteps / (pointCount − 1) / 0.02) : 0
```

```
score = clamp(round(continuity + duration + density + airborne + endpoints − truncation − implausible), 0, 100)
class = score ≥ 70 ? "strong" : score ≥ 40 ? "usable" : "weak"
```

`round` is round-half-up (JavaScript `Math.round`).

The stored quality object holds the version (1), the score, the class and every factor above.

---

## 7. Point simplification (simplification version 1)

Selects the points each leg keeps; kept points are unchanged.

### 7.1 Reconstruction contract

A dropped point is reconstructed from the two kept points that bracket it:

- **horizontally:** by great-circle interpolation at its time fraction;
- **vertically:** by linear interpolation of altitude at the same fraction.

### 7.2 Error bands

Each candidate point is judged against the band for **its own** altitude:

| Altitude band | Horizontal bound | Vertical bound |
| --- | --- | --- |
| below 10,000 ft (including `"ground"`, taken as 0 ft) | 25 m | 50 ft |
| 10,000 ft to below 18,000 ft | 60 m | 100 ft |
| 18,000 ft and above | 150 m | 200 ft |

A null altitude is banded as 10,000 ft.

### 7.3 Algorithm

1. **Short legs.** A leg of 2 points or fewer keeps every point.
2. **Structural anchors** are always kept:
   - the first and last points;
   - both points of every step with `step ≥ 120 s`;
   - both points wherever the altitude kind changes between consecutive points. The kinds are `"ground"`, numeric and null.
3. **Refinement.** For each pair of consecutive anchors `(a, b)` with at least one point between them:
   1. For every interior point `i`, compute the normalised error `e = max(h / H, v / V)`, where:
      - `h` is the great-circle distance in metres between point `i` and its reconstruction from `a` and `b`;
      - `v` is the absolute vertical reconstruction error, counting `"ground"` as 0. It is computed only when `i`, `a` and `b` all have a numeric or ground altitude; otherwise the vertical term is 0;
      - `H` and `V` are the bounds of point `i`'s band.

      A non-finite `e` is treated as infinite.
   2. Let `w` be the interior point with the largest `e`. On ties, take the lowest index.
   3. If `e(w) > 1`, keep `w` and repeat the refinement on `(a, w)` and on `(w, b)`.
4. **Result.** The retained points are the anchors plus every point kept in step 3, in time order.

---

## 8. Corpus record

Each kept leg stores its §5 attributes, §6 quality object, point counts before and after simplification, and kept points with their §2.3 fields. Each day records its source release, versions and simplification parameters.

---

## 9. Physical-flight records

### 9.1 Selection criterion

A corpus leg becomes a physical-flight record, promoted under §10, only when associated with a customer flight record (association is outside the component, §0.3). The record stores no customer identity.

### 9.2 Derived attributes

| Attribute | Rule |
| --- | --- |
| `icao24`, `callsign` | From the leg (§5) |
| `corpusDay`, `serviceDate` | The UTC date of `observedStartAt` |
| `observedDepartureAt`, `observedArrivalAt` | `observedStartAt`, `observedEndAt` |
| origin and destination airport | The leg's `observedOrigin` and `observedDestination`, resolved to the airport reference (`additional-contents.md` §1) |
| `sourceQuality` | `strong` → `high`, `usable` → `medium`, `weak` → `low` |
| `coverage` | `coverageRatio ≥ 0.9` → `full`; `≥ 0.5` → `partial`; otherwise `sparse` |
| `anomalies` | `truncated_start` if `truncatedStart`; `truncated_end` if `truncatedEnd`; `gap_over_10min` if `maxGapSeconds > 600` |

---

## 10. Geometry promotion (geometry processing 2, phase logic 1, runway inference 2, frame contract 1)

### 10.1 Inputs

- **Points:** the corpus record's points, decoded verbatim, `n` points indexed `0 … n−1`.
- **Gap facts:** `G = 120` s, and the record's `durationSeconds`, `maxGapSeconds` and `coverageRatio` (§6.1, measured before simplification).
- **Departure and arrival airports:** the record's origin and destination (§9.2), from the airport reference (`additional-contents.md` §1):
  - `code` = IATA if present, else ICAO;
  - ICAO, latitude, longitude and field elevation in ft MSL (may be null).
- **Runway references** for those airports (`additional-contents.md` §2), possibly none.

### 10.2 Promotion fails

The leg is unpromotable when:

- it has fewer than 2 points;
- its times are not strictly increasing;
- no point has a non-`"ground"` altitude;
- the airborne envelope (§10.4) has no numeric altitude.

### 10.3 Observation-gap identification

Each step `s_i` (point `i−1` → `i`) is classified as `continuous`, `gap` or `unverified`:

1. **Uncovered time.** Let `U = max(0, D − coverageRatio · D)`, where `D` is `durationSeconds`.
2. **No raw gaps.** If `maxGapSeconds ≤ G`, every step is continuous (method `no_raw_gaps`).
3. **Constants.** Let tolerance `τ = 0.001 s`.
   - A step is "max" if `|s − maxGapSeconds| ≤ τ`.
   - Let `B = U − maxGapSeconds + 3τ`.
4. **Candidates** are steps with `G < s ≤ maxGapSeconds + τ`.
   - A candidate is **live** if it is max and `s ≤ U + τ`, or if it is not max and `s ≤ B`.
5. **No search possible.** If there are no live candidates, or more than **36**, every candidate is `unverified` (method `unidentifiable`).
6. **Search.** A valid solution is a subset of live candidates that:
   - sums to within `τ` of `U`;
   - contains at least one max step.

   All valid solutions are considered. If none exists, every candidate is `unverified` (method `unidentifiable`).
7. **Classification.**
   - A live candidate in **every** valid solution is a `gap`.
   - One in **some** but not all is `unverified`.
   - One in **none** stays `continuous`, as do candidates that are not live.
   - The method is `identified`, or `partially_identified` if any step is `unverified`.

A `gap` or `unverified` step is **interrupted**.

### 10.4 Airborne envelope

1. **Airborne runs** are maximal runs of consecutive points whose altitude is not `"ground"`. Null altitude counts as not ground.
2. **Long runs** last at least **60 s** from first to last point.
3. **Envelope.**
   - `airStart` is the first index of the first long run. If there is none, it is the first index of the first run.
   - `airEnd` is the last index of the last long run. If there is none, it is the last index of the last run.

### 10.5 Contact events

- **Liftoff** exists if:
  - `airStart > 0`;
  - point `airStart−1` is `"ground"`;
  - step `airStart` is not interrupted.

  Its surface index is `airStart−1` and its airborne index is `airStart`.
- **Touchdown** exists if:
  - `airEnd < n−1`;
  - point `airEnd+1` is `"ground"`;
  - step `airEnd+1` is not interrupted.

  Its surface index is `airEnd+1` and its airborne index is `airEnd`.

For each contact:

- **`estimatedAt`** is the midpoint of the two sample times.
- **`intervalSeconds`** is the absolute difference between the two sample times.
- **Confidence** is set in this order:
  1. Start from the interval: high if ≤ **10 s**; medium if ≤ **30 s**; otherwise low.
  2. Low if either sample is stale.
  3. Low if an airport is known and the surface sample is more than **6,000 m** from its reference point.
  4. If no airport is known, a high value becomes medium.
- **`onAssignedRunway`** is whether the surface sample is on the usable runway (§10.6). It is null when there is no usable runway.

### 10.6 Runway inference (runway inference version 2)

#### Evidence

Per operation, stale points excluded; the edge is `airStart` (departure) or `airEnd` (arrival).

- **Surface side.** A point counts if it is `"ground"` and within **180 s** of the edge, and is:
  - before `airStart` (departure); or
  - after `airEnd` (arrival).

  The contact's surface point is marked `contact`.
- **Airborne side.** A point counts if its altitude is numeric and it is within **300 s** of the edge, and is:
  - at or after `airStart` (departure); or
  - at or before `airEnd` (arrival).

  Its height above the field is its barometric altitude minus the field elevation, with an unknown elevation taken as 0.
- **Motion bearing** for each evidence point, in order of preference:
  1. its track, normalised to [0, 360);
  2. else the initial bearing from the previous point, if that point is at least **5 m** away;
  3. else the bearing to the next point, if that point is at least **5 m** away;
  4. else null.

#### No assignment

| Condition | Status | Reason |
| --- | --- | --- |
| No airport known | `unavailable` | `no_airport` |
| No runway reference for the airport | `unavailable` | `no_runway_reference` |
| No evidence points | `unassigned` | `no_evidence` |

#### Airport frame

The airport's east-north-up frame (§10.13).

#### Runway directions considered

- **Each runway record** is used unless:
  - it is closed;
  - either end has non-finite coordinates;
  - both ends are identical.
- **Two directions** are taken from each record, from the low end to the high end and the reverse:
  - the start and end points are the end coordinates in the airport frame;
  - the bearing is the true bearing from start to end;
  - the length is the horizontal distance;
  - the half-width is `width_ft · 0.3048 / 2`, or **23 m** if the width is missing or ≤ 0;
  - the threshold offset is `displaced_threshold_ft · 0.3048` of the start end, or 0.
- Directions shorter than **100 m** are skipped.

#### Scoring each direction

For each evidence point, convert it to the frame at height 0 MSL. Compute:

- `along`: signed distance from the start along the runway line;
- `cross`: absolute distance perpendicular to it;
- `headingOk`: the motion bearing is known and within **15°** of the runway bearing (full bearing difference, not axis).

Then:

- **Surface point.**
  - It is on the paved area if `cross ≤ halfWidth + 30 m` and `−300 m ≤ along ≤ length + 300 m`.
  - If it is on the paved area and `headingOk`, it counts as aligned, and as surface-aligned; its `cross` is recorded.
  - If it is on the paved area and is the contact point, the direction has `contactOnRunway` (regardless of `headingOk`).
- **Airborne point.**
  - Its distance is `max(0, −along)` for an arrival, or `max(0, along − length)` for a departure.
  - It is skipped if the distance exceeds **8,000 m**, or if its height above the field is known and exceeds **2,000 ft**.
  - It counts as aligned if `cross ≤ 60 m + 0.03 · distance` and `headingOk`; its `cross` is recorded.

#### Choosing the runway

1. Keep the directions with at least one aligned point. If none remain, the result is `unassigned`, reason `no_aligned_runway`.
2. Sort them by:
   1. `contactOnRunway`, true first;
   2. aligned count, descending;
   3. median recorded `cross`, ascending;
   4. the start end's designator, by locale string comparison.
3. The first is **best**. The **runner-up** is the first later direction that belongs to a different runway record.
4. The result is **ambiguous**, and so `unassigned` with reason `ambiguous` and no runway, if all of these hold:
   - the runner-up exists;
   - it has the same `contactOnRunway` as best;
   - its aligned count is ≥ best's;
   - its median cross ≤ `max(best median cross, 1 m) · 1.5`.
5. Otherwise the result is `assigned`:
   - the runway is the best direction's start-end designator;
   - the runway pair is `"<low ident>/<high ident>"`.

#### Confidence of an assigned runway

- **High** if either:
  - `contactOnRunway`, at least 1 surface-aligned point and at least **2** aligned points; or
  - at least **3** airborne aligned points.
- **Medium** if there are at least **2** aligned points.
- **Low** otherwise.

#### Recorded evidence

The assignment also records:

- the evidence count, aligned count, surface-aligned count and `contactOnRunway`;
- the median cross-track and the median heading error over all evidence points with a bearing, each rounded to 0.1;
- the runner-up's designator and its median cross-track;
- the reference source string;
- the inference version (2).

#### Usable runway

A runway is **usable** for the rest of §10 when it is assigned with confidence medium or high.

#### On the runway

A point is "on the runway" of a usable runway direction if:

- `cross ≤ halfWidth + 30 m`; and
- `−60 m ≤ along ≤ length + 60 m`.

### 10.7 Takeoff roll and runway exit

#### Departure roll

The departure roll exists only when a liftoff exists.

- **Reference basis.** This applies when there is a usable departure runway and the liftoff surface point is on it. Walk backwards from the liftoff surface point. Stop at a point that:
  - is not `"ground"`;
  - is followed by an interrupted step; or
  - is not on the runway.

  The roll starts at the earliest point reached (basis `runway_reference`).
- **Trajectory basis.** This applies otherwise. Let the axis be the motion bearing at the liftoff surface point; if it is null, the roll starts at the liftoff surface point. Otherwise walk backwards, and stop at a point that:
  - is not `"ground"`;
  - is followed by an interrupted step;
  - has no motion bearing; or
  - deviates from the axis by more than **20°** (axis difference).

  A point with ground speed below **30 kt** is included as the roll start, and the walk stops there (basis `trajectory`).

#### Runway exit

The runway exit exists only when a touchdown exists.

- **Reference basis.** This applies when there is a usable arrival runway and the touchdown surface point is on it. Walk forward from the touchdown surface point, stopping at an interrupted step:
  - a point on the runway becomes the last on-runway point, and clears any pending off-runway point;
  - the first off-runway point after that is held as pending;
  - a second consecutive off-runway point ends the walk. The pending point is the first off-runway point.

  If the walk ends while a point is pending, that point is the first off-runway point.
- **Trajectory basis.** The same walk applies, except that "on the runway" means the motion bearing is known and within **20°** (axis difference) of the motion bearing at the touchdown surface point.
- **Exit confidence.**
  - Reference basis: high if the time from the last on-runway point to the first off-runway point is ≤ **20 s**; medium if a first off-runway point exists; otherwise low.
  - Trajectory basis: medium if a first off-runway point exists and that time is ≤ 20 s; otherwise low.

#### Primary path ends

- `primaryStart` is the roll start, or `airStart` if there is no roll.
- `primaryEnd` is the last on-runway point of the exit, or `airEnd` if there is no exit.

### 10.8 Primary path: observed segments and gap connectors

1. Walk the steps from `primaryStart + 1` to `primaryEnd`.
2. Each interrupted step `i` closes an observed segment ending at `i−1`. It then adds a **gap connector** from `i−1` to `i`:
   - **continuity:** `bridge` if the step lasts ≤ **600 s**, otherwise `break`;
   - **reason:** `observation_gap` for a `gap` step, `unverified_span` for an `unverified` step;
   - the duration and the great-circle distance.
3. **Bridge vertices.** A bridge carries interior great-circle vertices at fractions `k / m` for `k = 1 … m−1`, where `m = ceil(distance / 20,000 m)`. A break carries none. Altitude along a bridge is interpolated linearly between the two bounding points.
4. The next observed segment starts at `i`. The last segment ends at `primaryEnd`.

Connectors have class `calculated`.

### 10.9 Runway tie-ins (inferred)

A tie-in is considered only when all of these hold:

- the departure has no liftoff (or the arrival has no touchdown);
- the runway is assigned with **high** confidence.

The observed point is `primaryStart` for a departure, or `primaryEnd` for an arrival.

1. **Height check.** If the point's altitude is numeric and its height above the field exceeds **2,000 ft**, there is no tie-in. Height above the field is barometric altitude minus field elevation, with an unknown elevation taken as 0.
2. **Anchor distance along the runway.** Let `along` be the point's distance along the runway direction, and `tdz = thresholdOffset + 300 m`.
   - **Arrival:** `anchorAlong = min(max(along, tdz), max(tdz, length − 300 m))`.
   - **Departure:** `anchorAlong = min(max(along, 300 m), length)`.
3. **Anchor.** The anchor is the centreline point at `anchorAlong`, at height 0 in the frame, converted back to latitude and longitude.
4. **Distance checks.** Let the distance be the great-circle distance from the anchor to the point.
   - If it exceeds **6,000 m**, there is no tie-in.
   - If it is ≤ **50 m** and the point is `"ground"`, there is no tie-in.
5. **Anchor elevation.**
   - The runway elevation at `anchorAlong` is interpolated linearly between the two ends' published elevations, with the fraction clamped to [0, 1]. If only one end has an elevation, that elevation is used.
   - If neither end has one, the field elevation is used.
   - The source is recorded as `runway_threshold` or `airport_reference`.
6. **Estimated duration.** `distance / (groundSpeed · 0.514444)` when the point's ground speed is > 0, else null.

A tie-in has class `inferred`; it precedes the first segment (departure) or follows the last (arrival).

### 10.10 Taxi segments

The candidate ranges are:

- **Taxi-out:** points `0 … primaryStart−1`, checked against the departure airport;
- **Taxi-in:** points `primaryEnd+1 … n−1`, checked against the arrival airport.

Each range is split at interrupted steps. A piece is omitted, with the first reason that applies, if:

1. it has fewer than **2** points (`too_few_points`);
2. it has fewer than **2** non-stale points (`stale_positions`);
3. it lasts less than **20 s** (`too_short`);
4. its path length is less than **50 m** (`no_movement`);
5. an airport is known and any point is more than **6,000 m** from its reference point (`outside_airport`).

Otherwise the piece is kept as an observed taxi segment, with its path length and duration.

### 10.11 Phases (phase logic version 1)

#### Inputs

- `dep` and `arr` are the departure and arrival field elevations, with unknown taken as 0.
- Altitude means numeric barometric altitude.
- `maxAlt` is the largest altitude in `[airStart, airEnd]`.

If there is no altitude in that range, only the taxi, takeoff and landing ranges can arise.

#### Boundaries

1. **`climbStart`.**
   - With altitudes: the first `i` in `[airStart, airEnd]` with `alt − dep ≥ 1,000 ft`, else `airEnd + 1`.
   - Without altitudes: `airStart`.
2. **Takeoff observed** if either:
   - a departure roll exists; or
   - there are altitudes and `alt(airStart) − dep < 1,000 ft`, where a non-numeric `alt(airStart)` counts as +∞.
3. **Cruise.**
   - The band is `max(1,500 ft, 0.07 · maxAlt)`.
   - `cruiseStart` is the first index in `[airStart, airEnd]` with `alt ≥ maxAlt − band`; `cruiseEnd` is the last such index.
   - Then set `cruiseStart = max(cruiseStart, climbStart)`.
4. **`lastAtOrAbove(h)`.** Scan from `airEnd` down to `max(cruiseEnd, airStart)`. Return the first index with `alt − arr ≥ h`, or −1.
5. **`approachStart`.** Let `x = lastAtOrAbove(3,000 ft)`. `approachStart = x + 1` if `0 ≤ x < airEnd`; otherwise there is none.
6. **`landingStart`.** Let `y = lastAtOrAbove(500 ft)`.
   - If `0 ≤ y < airEnd`: `landingStart = y + 1`.
   - Else, if `y < 0` and `alt(airEnd) − arr < 500 ft` (non-numeric counts as +∞): `landingStart = max(cruiseEnd + 1, airStart)`.

#### Ranges, assembled in order

A range is skipped if its start is < 0, its end is < its start, or its end is ≥ `n`.

- **`taxi_out`** `[0, primaryStart−1]`, if `primaryStart > 0`.
- **`takeoff`** `[primaryStart, min(climbStart, airEnd+1) − 1]`, if takeoff was observed.
- **With a cruise** (`cruiseStart ≥ 0`):
  - **`climb`** `[c, cruiseStart − 1]`, where `c` = `climbStart` if takeoff was observed, else `airStart`;
  - **`cruise`** `[cruiseStart, cruiseEnd]`;
  - **`descent`** `[cruiseEnd + 1, e − 1]`, where `e` = `approachStart` if present, else `landingStart` if present, else `airEnd + 1`.
- **`approach`** `[approachStart, (landingStart if present else airEnd+1) − 1]`, if `approachStart` is present.
- **`landing`** `[landingStart if present else airEnd+1, primaryEnd]`, if `landingStart` is present or a runway exit exists.
- **`taxi_in`** `[primaryEnd+1, n−1]`, if `primaryEnd < n−1`.

#### Clean-up

Drop any range whose start is ≤ the end of the previous kept range. Each kept range stores its start and end indices and times.

### 10.12 Stored display-altitude reconciliation

Stored beside the raw altitudes, which are unchanged.

#### Altitude series

- Among points with numeric barometric altitude, if at least **80 %** also have geometric altitude, the series is `geometric`.
- Otherwise it is `barometric`.

#### Surface elevation per side

- If the side has a usable runway and a contact, use the runway elevation at the contact surface point's along-runway position (as in §10.9 step 5), when the runway publishes one. The source is `runway_threshold`.
- Otherwise use the field elevation, if known. The source is `airport_reference`.
- Otherwise there is none.

#### Reconciliation for each contact

A reconciliation is made for a contact only when:

- the contact exists and its confidence is **high**;
- a surface elevation exists;
- `onAssignedRunway` is not false;
- the airborne sample has a value in the chosen series.

With `value` that series value, `half = intervalSeconds / 2`, and `rate` the airborne sample's vertical rate (trace element 7):

```
signedHalf = touchdown ? +half : −half
estimate   = rate is known ? value + rate · signedHalf / 60 : value
offsetFt   = surfaceElevationFt − estimate
uncertainty = rate is known ? |rate · half / 60| : 50 ft
blend      = linear in time, 120 s
```

The record also stores:

- the event and the series;
- the surface elevation, its source and its reference (`msl`);
- the airborne sample's index and its raw barometric and geometric altitudes, copied verbatim;
- `estimate`, `offsetFt`, `uncertainty` and `blend`.

### 10.13 Coverage, statistics, anomalies and frames

#### Coverage

- **`sparse`** if `primaryEnd − primaryStart + 1 < 10`, or if the observed segment time divided by `t(primaryEnd) − t(primaryStart)` is below **0.5**.
- **`full`** if the path starts at a runway (liftoff or departure tie-in), ends at a runway (touchdown or arrival tie-in), and has no breaks and no unverified connectors.
- **`partial`** otherwise.

The record also stores:

- `startsAtRunway`, `endsAtRunway`;
- observed and primary-span seconds;
- the bridge, break and unverified counts;
- the gap-identification method.

#### Statistics

- the point count;
- the first and last times;
- the minimum and maximum numeric barometric altitude;
- the bounding box of all points.

#### Anomalies

- `gap_over_10min` (any break);
- `unverified_observation_spans`;
- `departure_contact_not_observed`;
- `arrival_contact_not_observed`.

#### Airport local frames (frame contract version 1)

Each frame is anchored at the airport reference point:

| Field | Value |
| --- | --- |
| Identifier | `airport:<ICAO or code>` |
| Anchor height | Field elevation · 0.3048 m, or 0 if unknown |
| Vertical reference | `msl` |
| Anchor source | `ourairports:airports.iata.csv` |

Coordinates convert through WGS84 ECEF:

- semi-major axis 6,378,137 m, inverse flattening 298.257223563;
- ECEF to geodetic is the WGS84 inverse, evaluated to better than 1 mm;
- the east-north-up rotation is taken at the anchor.

A runway reference is used only if it was built in the same airport frame (same anchor, to 9 decimal places in latitude and longitude and 3 in height) as the flight's.

---

## 11. Level-of-detail derivation (LOD parameters version 1)

Every LOD is a subset of the master points, unchanged, with the master's segments, connectors, tie-ins, phases and contacts.

| LOD | Content |
| --- | --- |
| `master` | Every observed point, plus all structure. Error 0. |
| `regional`, `global` | Only points inside the primary path's observed segments, simplified under the phase bounds below. Taxi structure is omitted. Phases are kept only when both their start and end points are retained. |
| `surface` | Only the points of the kept taxi segments, unsimplified, with the taxi phases. Not produced when there are no taxi segments. |

### 11.1 Simplification for `regional` and `global`

1. **Structural anchors** are never dropped:
   - every observed-segment start and end;
   - every connector end;
   - every tie-in's observed point;
   - every phase start and end;
   - each contact's surface and airborne points;
   - the roll start;
   - the exit's last-on and first-off points;
   - each reconciliation's airborne point.
2. **Refinement.** Each observed segment `[from, to]` is refined with the same worst-point method as §7.3. The fixed points are `from`, every anchor strictly inside, and `to`. These differences apply:
   - each point's bound comes from the phase that contains it, or from `unphased` if no phase does;
   - the vertical error counts only when the point and both bracketing points have **numeric** barometric altitude; ground does not count here;
   - the reconstruction is the same: great-circle position and linear altitude, by time fraction.

### 11.2 Bounds (horizontal m / vertical ft)

| Phase | regional | global |
| --- | --- | --- |
| taxi_out | 10 / 25 | 100 / 200 |
| takeoff | 25 / 50 | 150 / 200 |
| climb | 100 / 150 | 600 / 600 |
| cruise | 500 / 500 | 3,000 / 1,500 |
| descent | 100 / 150 | 600 / 600 |
| approach | 50 / 75 | 300 / 300 |
| landing | 25 / 50 | 150 / 200 |
| taxi_in | 10 / 25 | 100 / 200 |
| unphased | 100 / 150 | 600 / 600 |

There is no point budget or point cap per LOD.

### 11.3 Recorded with each LOD

- the retained master indices;
- the achieved maximum horizontal and vertical error;
- per-phase retained counts and errors;
- the parameter set.

The `global` artifact omits:

- contacts, roll, exit, frames and omitted taxi;
- runway-evidence residuals;
- reconciliations.

---

## 12. Stored summary

Each promoted physical flight stores its geometry version, coverage class, runway assignments and confidence, altitude range, bounding box, and a summary restating the §10–§11 results, with lengths, durations and offsets rounded.

---

## 13. Not covered by this method

Everything listed in §0.3.
