Skip to content

Read GeoJSON fallback coordinates in GeoJSON order - #284

Merged
roncodes merged 1 commit into
dev-v0.6.59from
fix/geojson-coordinate-order
Aug 3, 2026
Merged

Read GeoJSON fallback coordinates in GeoJSON order#284
roncodes merged 1 commit into
dev-v0.6.59from
fix/geojson-coordinate-order

Conversation

@roncodes

@roncodes roncodes commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Utils::getPointFromMixed() returned latitude and longitude the wrong way round for one class of GeoJSON input. This corrects it and pins the behaviour with tests.

Found while writing coverage for the two fallback arms on dev-v0.6.59; split out into its own PR because it is a behaviour change on the location write path rather than a coverage commit.

The defect

Support/Utils.php resolves a GeoJSON envelope in two passes. The first, pointFromGeoJson(), handles well-formed Point and Feature-wrapped Point values and maps them correctly — coordinates[0] to longitude, coordinates[1] to latitude.

Anything it declines falls through to Point::fromJson(), and when that throws, two fallback arms recursed with the bare coordinate value:

if ($coordinatesInGeoJson) {
    return static::getPointFromMixed($coordinatesInGeoJson);
}

if ($coordinatesInGeoJsonFeature) {
    return static::getPointFromMixed($coordinatesInGeoJsonFeature);
}

Recursing hands the pair to the positional array reader further down the same method:

$latitude  = static::or($coordinates, ['_lat', 'lat', '_latitude', 'latitude', 'x', '0']);
$longitude = static::or($coordinates, ['lon', '_lon', 'long', 'lng', '_lng', '_longitude', 'longitude', 'y', '1']);

Index 0 is read as the latitude and 1 as the longitude — the reverse of GeoJSON's [lng, lat]. So a pair reaching either arm came back transposed.

Which inputs were affected

Narrower than it first looks, because well-formed points never reach the fallback — pointFromGeoJson() returns early for them. Reaching it requires an envelope that isGeoJson() accepts but Point::fromJson() rejects, with a coordinate value that is still a single pair. In practice:

  • a Point envelope carrying extra members that Point::fromJson() will not parse
  • a multi-coordinate type (LineString, MultiPoint, …) whose coordinates is a flat [lng, lat] pair rather than an array of pairs — malformed, but the kind of thing hand-built payloads and looser API clients produce

Polygon and LineString rings — nested arrays — were not affected and are not affected now; they fall through to the same positional reader as before.

The fix

Read the fallback value as GeoJSON before falling back to positional interpretation, reusing the mapping that is already correct:

if ($coordinatesInGeoJson) {
    return static::pointFromGeoJsonCoordinates($coordinatesInGeoJson)
        ?? static::getPointFromMixed($coordinatesInGeoJson);
}

pointFromGeoJsonCoordinates() is a thin wrapper over the existing pointFromGeoJson(), which already declines anything that is not a usable numeric pair. So nested rings return null there and still reach the old recursion untouched — only the bare-pair case changes, which is the defective one.

Tests

server/tests/PointResolutionTest.php previously asserted the transposed result, with a comment recording that it was documenting the defect rather than the intent. That expectation is flipped here, deliberately — this is a behaviour change, so the test moves with it.

Two cases are added alongside: the top-level coordinates arm is preferred over nested geometry.coordinates and read in the same order, and a Polygon ring still routes to the positional reader.

Verification

Both fallback arms and both sides of the ?? are covered — confirmed by slice coverage, not just passing assertions. The full suite runs clean apart from OrderControllerUpstreamNotFoundTest, which fails by design on dev-v0.6.59 until core-api 1.6.55 ships.

Worth a manual MySQL pass before this merges. It sits on the location write path, so zones, service areas, waypoint and place location save/update, and reverse geocoding are the things to exercise. The in-memory SQLite suite proves the branch logic, not real-database behaviour.

getPointFromMixed() resolves a GeoJSON envelope in two passes. When the first
declines and Point::fromJson() then throws, two fallback arms recursed with
the bare coordinate value — which hands it to the positional array reader in
the same method, where index 0 is read as the latitude and 1 as the
longitude. That is the reverse of GeoJSON's [lng, lat], so any pair reaching
either arm came back transposed.

Well-formed points never get that far: pointFromGeoJson() returns early for
them. Reaching the fallback needs an envelope isGeoJson() accepts but
Point::fromJson() rejects, whose coordinate value is still a single pair — a
Point carrying extra members, or a multi-coordinate type whose coordinates is
a flat pair rather than an array of pairs.

Both arms now read the value as GeoJSON first, through a thin wrapper over the
existing pointFromGeoJson(), whose mapping is already correct. It returns null
for anything that is not a usable numeric pair, so nested Polygon and
LineString rings decline there and still fall through to the old recursion
untouched. Only the bare-pair case changes.

PointResolutionTest asserted the transposed result with a comment recording
that it documented the defect rather than the intent. That expectation is
flipped here, deliberately, since this is a behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@roncodes
roncodes merged commit 3f5ecd6 into dev-v0.6.59 Aug 3, 2026
@roncodes
roncodes deleted the fix/geojson-coordinate-order branch August 3, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant