How do you filter a stream of location fixes: accuracy, age, and implausible jumps?

17.9.1 · L3 · Interview

Predict first

Which fixes survive?

import Foundation
import CoreLocation

struct Fix { let lat: Double; let lon: Double; let acc: Double; let age: TimeInterval }
let now = Date(timeIntervalSince1970: 1_700_000_000)
let raw: [Fix] = [
    Fix(lat: 18.5204, lon: 73.8567, acc: 12, age: 1),     // good
    Fix(lat: 18.5205, lon: 73.8568, acc: 8, age: 1),      // good
    Fix(lat: 18.5204, lon: 73.8567, acc: -1, age: 1),     // negative accuracy: invalid
    Fix(lat: 18.5206, lon: 73.8569, acc: 10, age: 300),   // cached five minutes ago
    Fix(lat: 18.5300, lon: 73.8600, acc: 900, age: 1),    // too coarse
    Fix(lat: 19.0760, lon: 72.8777, acc: 10, age: 1),     // 120 km away one second later: a jump
    Fix(lat: 18.5207, lon: 73.8570, acc: 9, age: 1),      // good again
]
var last: CLLocation?
var kept: [Int] = []
for (i, f) in raw.enumerated() {
    let loc = CLLocation(coordinate: CLLocationCoordinate2D(latitude: f.lat, longitude: f.lon), altitude: 0,
                         horizontalAccuracy: f.acc, verticalAccuracy: -1, course: -1, speed: -1,
                         timestamp: now.addingTimeInterval(Double(i) - f.age))
    var why = ""
    if loc.horizontalAccuracy < 0 { why = "invalid accuracy" }
    else if now.timeIntervalSince(loc.timestamp) > 10 { why = "stale" }
    else if loc.horizontalAccuracy > 100 { why = "too coarse" }
    else if let l = last, loc.distance(from: l) / max(loc.timestamp.timeIntervalSince(l.timestamp), 1) > 70 { why = "implausible speed" }
    print(i, why.isEmpty ? "keep" : "drop: " + why)
    if why.isEmpty { last = loc; kept.append(i) }
}
print("kept:", kept)
  • 0, 1 and 6 are kept; the others are invalid, stale, coarse or a jump
  • All of them
  • Only the first

Mental model. Fixes are witnesses: ignore the one who says he is unsure, the one reporting old news, and the one who claims to have teleported.

Every CLLocation has a horizontalAccuracy (radius in metres, negative means invalid), a timestamp, and optional speed and course (also -1 when unknown). The first fixes after starting updates may be cached and old, so a robust consumer drops a fix that is invalid, older than a few seconds for a live feature, too coarse for the use case, or physically implausible relative to the last kept fix (distance divided by elapsed time above anything the user could do). The run feeds seven constructed fixes through exactly these rules and keeps indexes 0, 1 and 6; the 120 km jump after one second is dropped by the speed test, not by accuracy.

Choose thresholds per feature (a running app wants 20 to 50 m, a store locator 500 m), prefer asking the system for less (desiredAccuracy, distanceFilter) before filtering, and smooth with care: averaging coordinates hides turns, while weighting by 1/accuracy² is a simple improvement. The thresholds in the run are illustrative choices, not Apple values, and real fixes from a device were not collected.

Accuracy, age and speed checks remove bad fixes.

Common traps

  • Using the first fix. It may be old; check the timestamp.
  • Ignoring negative accuracy. It means the coordinate is invalid.

They might follow up with

  • How far is a jump “implausible”? Compare implied speed with the activity type.

Related questions

  • 17.2.1 How does Core Location work: authorization levels, accuracy, significant-change and visits, and geofencing?

How do you geofence 500 stores when an app may monitor only 20… →