Workout route map double-converts the first ~49 samples on mainland-China tracks (FB25306175)

Every ride I import into HealthKit from a Chinese bike computer renders wrong at the
start in the Fitness and Health apps, and the same record is wrong on iOS 26 as well as
on iOS 27.2 (iPhone 15 Pro, mainland China, AutoNavi base map). Filed as FB25306175.

WHAT HAPPENS

The first ~49 samples of an HKWorkoutRoute are drawn about 415 m east and 222 m south of
the coordinates actually stored, and a straight line joins that piece back to the rest of
the track. The green start marker is on the displaced piece. Every remaining sample, and
the red end marker, are drawn exactly where stored.

On this record (2794 points, 1 Hz, 17649 m drawn) the displaced block is the first 209 m
of riding — about 53 seconds The displacement is one more application of the local
WGS-84 → GCJ-02 transform, which these coordinates already contain: measured from
screenshots at native 1179×2556, 417 m E / 219 m S on the full-screen map and 418 m E /
220 m S on the summary map, against a true local offset of 415.2 m E / 222.4 m S.

THE STORED DATA IS CORRECT

  • HKWorkoutRouteQuery returns all 2794 CLLocation values unchanged; the descriptor query
    returns the same count and the same first/last values. One workout, one route.
  • The same coordinates drawn with SwiftUI Map + MapPolyline on the same phone render
    correctly end to end, including the start.
  • The same unchanged workout, opened twice with no write in between, was correct the
    first time and offset after leaving and re-entering. So the data is not the variable.

THE WRITE PATH

The head unit writes GCJ-02 into record.position_lat / position_long. That is what makes
the track land on the road here: MKMapSnapshotter on the mainland base map puts the
GCJ-02 values exactly on two known intersections, and the WGS-84 values about 470 m
north-west of them, and MapKit does no conversion for us. So they are written verbatim:

let config = HKWorkoutConfiguration()
config.activityType = .cycling
config.locationType = .outdoor
let builder = HKWorkoutBuilder(healthStore: store, configuration: config, device: nil)
try await builder.beginCollection(at: firstPoint.timestamp.addingTimeInterval(-1))

// CLLocation per record, coordinates as recorded, horizontalAccuracy 10,
// verticalAccuracy 10 (or -1 where the record has no altitude), recorded altitude
// and speed, course computed from the previous point.
let routeBuilder = builder.seriesBuilder(for: HKSeriesType.workoutRoute()) as? HKWorkoutRouteBuilder
for batch in locations.chunked(into: 1000) {
    try await routeBuilder?.insertRouteData(batch)
}
try await builder.endCollection(at: lastPoint.timestamp.addingTimeInterval(10))
let workout = await builder.finishWorkout()

WHAT IS NOT THE CAUSE

Established with controlled synthetic routes written from the same start point:

probe sample interval m per point displaced length displaced time samples
E 1 s 10 ~480 m ~48 s ~49
F 2 s 10 ~480 m ~96 s ~49
G 1 s 20 ~950 m ~48 s ~49

The block follows SAMPLE COUNT, not time and not distance. Also ruled out, each by
writing a variant: lap and pause/resume events (present, absent, and a 206-point probe
carrying all three); the first point's metadata (altitude 0 / course -1 /
verticalAccuracy -1); valid versus -1 values for horizontalAccuracy, verticalAccuracy,
speed and course; dropping an isolated leading point; back-dating the first timestamp to
the workout start; trimming the leading 200 m (the offset simply re-appears at the new
start, which still begins with 49 samples). Writing WGS-84 instead inverts the symptom —
the leading block then looks right and everything else goes ~470 m north-west — which is
what you would expect if only the leading block is converted a second time.

SCOPE

This is not my one file. Three separate real rides, two cities, two head units, plus
synthetic routes, all behave identically:

MAGENE C706_54634 Suzhou 2026-10-23 17.61 km 2797 records / 2794 stored points
MAGENE C706_54634 Suzhou 2026-10-22 12.66 km 2076 records
MAGENE C606P_33206 Chengdu 2026-10-20 63.30 km 12069 records

Apple Watch recordings made in mainland China render correctly, and a Garmin Edge 1050
file recorded in the US imported through the same code path renders correctly. The common
factor is coordinates that are already GCJ-02 when they reach HKWorkoutRouteBuilder —
which is every Chinese bike computer, every Chinese running app, and every GPX export
from a Chinese service. So any third-party importer here hits this.

WHAT I AM ASKING

  1. Does the workout map apply a datum conversion to a leading block of route samples, and
    is that block size fixed?
  2. What is the official coordinate contract for HKWorkoutRoute locations for apps working
    with GCJ-02 data in mainland China?
  3. Is there a supported way to opt out, and what decides whether a given route enters
    this state?
  4. Which component owns the conversion — Health/Fitness route rendering or MapKit?

I am deliberately not pre-converting the first ~49 points: that would store mixed datums
in HealthKit, wrong for the Health app itself, for exports, for every third-party reader,
and wrong for all of them the moment this is fixed.

Happy to share the one-tap sample project, the exported JSON with all 2794 stored CLLocation
values, the original .fit, and the probe E/F/G screenshots — reply here and I'll attach them.

iOS 18.4 changed how Health and Fitness draw workout routes on the mainland-China base map. The same app and the same route data render differently depending only on the OS version. On iOS 17.5, 18.0 and 18.3.1 the whole track is drawn one GCJ-02 displacement away from its stored coordinates (≈415 m east, ≈221 m south at this location). On iOS 18.4, 18.6 and 27 only the first ~49 samples are displaced that way; every later sample is drawn at its stored coordinates, and a straight line joins the leading block back to the rest. The stored data is identical in all cases.
Version-dependent offset of a third-party HKWorkoutRoute drawn on the mainland-China map

Data: a cycling FIT file whose positions are GCJ-02 — the datum of Apple's mainland base map. Verified at 31.3013 N, 120.7418 E: the GCJ-02 value sits on the road centre line, the WGS-84 value lands ~470 m north-west, and fitting the ride against OSM roads improves from 52.8 m median to 8.3 m after converting GCJ-02 → WGS-84. The app writes every position verbatim into an HKWorkoutRoute (no conversion on our side).

iOS version What Health / Fitness draw
17.5, 18.0, 18.3.1 the entire track, offset by one GCJ-02 displacement (≈415 m E, ≈221 m S) from the stored coordinates
18.4, 18.6, 27.0 only the first ~49 samples are offset by one GCJ-02 displacement; every later sample is drawn at its stored coordinates, joined by a straight line
Further measurements:

The size of the affected block is a sample count (~49), not a time or distance: two recordings of the same geometry at 1 Hz and 2 Hz both shift ≈49 samples (≈48 s vs ≈96 s); with 20 m sample spacing the shifted block is ≈950 m long and still ≈49 samples.
The stored data is not the variable: HKWorkoutRouteQuery returns all 3,141 points unchanged, and a HealthKit export GPX matches the stored values within 1e-6 degrees.
For long real routes the offset is intermittent: the same route object sometimes renders correctly on first open and shows the shifted leading block after leaving the screen and re-entering it, with no write in between.
This behavior change is undocumented: the iOS 18.4 Release Notes contain no HealthKit, MapKit or CoreLocation section and no mention of coordinates or datum, and the same is true for 18.3, 18.5 and 18.6.
Expected: a route is drawn at its stored coordinates — as it effectively was on iOS 18.3 and earlier, where the conversion was applied to the whole track. Actual: since iOS 18.4 the conversion appears to be applied to the leading block only, so the head of the track is displaced by one GCJ-02 offset relative to the rest.

Minimal reproduction: install one build on an iOS 18.3.1 simulator and on an iOS 18.4 simulator; write one outdoor workout plus a 200+ sample HKWorkoutRoute that follows a known road using GCJ-02 coordinates; open the workout's map in Fitness on both. On 18.3.1 the whole line is offset; on 18.4 the head is offset and the rest is correct.