FAILURE MAP
← Case archive

FA-69916 / Map projection transforms / Open access

Geographic bounds of a map tile: tms row flip · case 01

TMS tiles report bounds one tile further south than their neighbours report.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Converting a TMS row to an xyz row uses n - row, an off-by-one that shifts every TMS tile by one row.

VERIFIED REPAIR

At the tms row flip step restore `row = n - 1 - row`, leaving the rest of the model unchanged.

Unsuccessful approach: The special-case for row 0 keeps the southernmost TMS row unflipped, so it reports the north edge tile.

Case contract

Input [z, col, row, scheme] where scheme is "xyz" (row 0 at north) or "tms" (row 0 at south). Return [west, south, east, north] in degrees rounded to 6 decimals, using lon = col/n*360-180 and lat = degrees(atan(sinh(pi*(1-2*r/n)))) for an xyz row edge r, n = 2**z. A tms row t is the xyz row n-1-t.

Why this case matters

Tile footprints drive cache invalidation, label placement and raster reprojection; wrong edges misalign tiles.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    z, col, row, scheme = x
    n = 2 ** z
    if scheme == 'tms':
        row = n - row
    def edge_lat(r):
        return math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * r / n))))
    west = col / n * 360.0 - 180.0
    east = (col + 1) / n * 360.0 - 180.0
    north = edge_lat(row)
    south = edge_lat(row + 1)
    return [round(west, 6), round(south, 6), round(east, 6), round(north, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #1', [1, 1, 0, 'xyz'], [0.0, 0.0, 180.0, 85.051129]), ('control #2', [2, 3, 3, 'tms'], [90.0, 66.51326, 180.0, 85.051129]), ('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #4', [4, 7, 9, 'tms'], [-22.5, 21.943046, 0.0, 40.979898]), ('control #5', [5, 16, 10, 'xyz'], [0.0, 48.922499, 11.25, 55.776573]), ('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321])], [('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #4', [4, 7, 9, 'tms'], [-22.5, 21.943046, 0.0, 40.979898]), ('control #5', [5, 16, 10, 'xyz'], [0.0, 48.922499, 11.25, 55.776573]), ('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #7', [7, 100, 40, 'xyz'], [101.25, 54.162434, 104.0625, 55.776573]), ('control #8', [8, 0, 255, 'xyz'], [-180.0, -85.051129, -178.59375, -84.928321]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361])], [('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #7', [7, 100, 40, 'xyz'], [101.25, 54.162434, 104.0625, 55.776573]), ('control #8', [8, 0, 255, 'xyz'], [-180.0, -85.051129, -178.59375, -84.928321]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #12', [12, 1206, 1540, 'xyz'], [-74.003906, 40.647304, -73.916016, 40.713956]), ('control #13', [3, 0, 7, 'tms'], [-180.0, 79.171335, -135.0, 85.051129])], [('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #12', [12, 1206, 1540, 'xyz'], [-74.003906, 40.647304, -73.916016, 40.713956]), ('control #13', [3, 0, 7, 'tms'], [-180.0, 79.171335, -135.0, 85.051129]), ('control #14', [1, 0, 1, 'tms'], [-180.0, 0.0, 0.0, 85.051129]), ('control #15', [9, 300, 200, 'xyz'], [30.9375, 36.031332, 31.640625, 36.597889])], [('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #1', [1, 1, 0, 'xyz'], [0.0, 0.0, 180.0, 85.051129]), ('control #2', [2, 3, 3, 'tms'], [90.0, 66.51326, 180.0, 85.051129]), ('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #14', [1, 0, 1, 'tms'], [-180.0, 0.0, 0.0, 85.051129]), ('control #15', [9, 300, 200, 'xyz'], [30.9375, 36.031332, 31.640625, 36.597889])]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
control #0[-180.0, -85.051129, 180.0, 85.051129][-180.0, -85.051129, 180.0, 85.051129]Passed
control #1[0.0, 0.0, 180.0, 85.051129][0.0, 0.0, 180.0, 85.051129]Passed
control #2[90.0, 0.0, 180.0, 66.51326][90.0, 66.51326, 180.0, 85.051129]Failed
control #3[45.0, 40.979898, 90.0, 66.51326][45.0, 40.979898, 90.0, 66.51326]Passed
control #4[-22.5, 0.0, 0.0, 21.943046][-22.5, 21.943046, 0.0, 40.979898]Failed
control #5[0.0, 48.922499, 11.25, 55.776573][0.0, 48.922499, 11.25, 55.776573]Passed
control #6[5.625, -55.776573, 11.25, -52.48278][5.625, -52.48278, 11.25, -48.922499]Failed
control #9[178.59375, -85.17097, 180.0, -85.051129][178.59375, -85.051129, 180.0, -84.928321]Failed

SHA-256 / 879c83ec14c392ec0bfabc4ce465b9b7dbc52662903623221aa6d3f462237bbf

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    z, col, row, scheme = x
    n = 2 ** z
    if scheme == 'tms':
        row = n - 1 - row if row > 0 else row
    def edge_lat(r):
        return math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * r / n))))
    west = col / n * 360.0 - 180.0
    east = (col + 1) / n * 360.0 - 180.0
    north = edge_lat(row)
    south = edge_lat(row + 1)
    return [round(west, 6), round(south, 6), round(east, 6), round(north, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #1', [1, 1, 0, 'xyz'], [0.0, 0.0, 180.0, 85.051129]), ('control #2', [2, 3, 3, 'tms'], [90.0, 66.51326, 180.0, 85.051129]), ('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #4', [4, 7, 9, 'tms'], [-22.5, 21.943046, 0.0, 40.979898]), ('control #5', [5, 16, 10, 'xyz'], [0.0, 48.922499, 11.25, 55.776573]), ('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321])], [('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #4', [4, 7, 9, 'tms'], [-22.5, 21.943046, 0.0, 40.979898]), ('control #5', [5, 16, 10, 'xyz'], [0.0, 48.922499, 11.25, 55.776573]), ('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #7', [7, 100, 40, 'xyz'], [101.25, 54.162434, 104.0625, 55.776573]), ('control #8', [8, 0, 255, 'xyz'], [-180.0, -85.051129, -178.59375, -84.928321]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361])], [('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #7', [7, 100, 40, 'xyz'], [101.25, 54.162434, 104.0625, 55.776573]), ('control #8', [8, 0, 255, 'xyz'], [-180.0, -85.051129, -178.59375, -84.928321]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #12', [12, 1206, 1540, 'xyz'], [-74.003906, 40.647304, -73.916016, 40.713956]), ('control #13', [3, 0, 7, 'tms'], [-180.0, 79.171335, -135.0, 85.051129])], [('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #12', [12, 1206, 1540, 'xyz'], [-74.003906, 40.647304, -73.916016, 40.713956]), ('control #13', [3, 0, 7, 'tms'], [-180.0, 79.171335, -135.0, 85.051129]), ('control #14', [1, 0, 1, 'tms'], [-180.0, 0.0, 0.0, 85.051129]), ('control #15', [9, 300, 200, 'xyz'], [30.9375, 36.031332, 31.640625, 36.597889])], [('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #1', [1, 1, 0, 'xyz'], [0.0, 0.0, 180.0, 85.051129]), ('control #2', [2, 3, 3, 'tms'], [90.0, 66.51326, 180.0, 85.051129]), ('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #14', [1, 0, 1, 'tms'], [-180.0, 0.0, 0.0, 85.051129]), ('control #15', [9, 300, 200, 'xyz'], [30.9375, 36.031332, 31.640625, 36.597889])]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
control #0[-180.0, -85.051129, 180.0, 85.051129][-180.0, -85.051129, 180.0, 85.051129]Passed
control #1[0.0, 0.0, 180.0, 85.051129][0.0, 0.0, 180.0, 85.051129]Passed
control #2[90.0, 66.51326, 180.0, 85.051129][90.0, 66.51326, 180.0, 85.051129]Passed
control #3[45.0, 40.979898, 90.0, 66.51326][45.0, 40.979898, 90.0, 66.51326]Passed
control #4[-22.5, 21.943046, 0.0, 40.979898][-22.5, 21.943046, 0.0, 40.979898]Passed
control #5[0.0, 48.922499, 11.25, 55.776573][0.0, 48.922499, 11.25, 55.776573]Passed
control #6[5.625, -52.48278, 11.25, -48.922499][5.625, -52.48278, 11.25, -48.922499]Passed
control #9[178.59375, 84.928321, 180.0, 85.051129][178.59375, -85.051129, 180.0, -84.928321]Failed

SHA-256 / 7fe3635633b094225fb727c016742e74fd5c45b00d6c8c6ccf8dc819cb67f161

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    z, col, row, scheme = x
    n = 2 ** z
    if scheme == 'tms':
        row = n - 1 - row
    def edge_lat(r):
        return math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * r / n))))
    west = col / n * 360.0 - 180.0
    east = (col + 1) / n * 360.0 - 180.0
    north = edge_lat(row)
    south = edge_lat(row + 1)
    return [round(west, 6), round(south, 6), round(east, 6), round(north, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #1', [1, 1, 0, 'xyz'], [0.0, 0.0, 180.0, 85.051129]), ('control #2', [2, 3, 3, 'tms'], [90.0, 66.51326, 180.0, 85.051129]), ('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #4', [4, 7, 9, 'tms'], [-22.5, 21.943046, 0.0, 40.979898]), ('control #5', [5, 16, 10, 'xyz'], [0.0, 48.922499, 11.25, 55.776573]), ('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321])], [('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #4', [4, 7, 9, 'tms'], [-22.5, 21.943046, 0.0, 40.979898]), ('control #5', [5, 16, 10, 'xyz'], [0.0, 48.922499, 11.25, 55.776573]), ('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #7', [7, 100, 40, 'xyz'], [101.25, 54.162434, 104.0625, 55.776573]), ('control #8', [8, 0, 255, 'xyz'], [-180.0, -85.051129, -178.59375, -84.928321]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361])], [('control #6', [6, 33, 21, 'tms'], [5.625, -52.48278, 11.25, -48.922499]), ('control #7', [7, 100, 40, 'xyz'], [101.25, 54.162434, 104.0625, 55.776573]), ('control #8', [8, 0, 255, 'xyz'], [-180.0, -85.051129, -178.59375, -84.928321]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #12', [12, 1206, 1540, 'xyz'], [-74.003906, 40.647304, -73.916016, 40.713956]), ('control #13', [3, 0, 7, 'tms'], [-180.0, 79.171335, -135.0, 85.051129])], [('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #10', [10, 550, 335, 'xyz'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #12', [12, 1206, 1540, 'xyz'], [-74.003906, 40.647304, -73.916016, 40.713956]), ('control #13', [3, 0, 7, 'tms'], [-180.0, 79.171335, -135.0, 85.051129]), ('control #14', [1, 0, 1, 'tms'], [-180.0, 0.0, 0.0, 85.051129]), ('control #15', [9, 300, 200, 'xyz'], [30.9375, 36.031332, 31.640625, 36.597889])], [('control #0', [0, 0, 0, 'xyz'], [-180.0, -85.051129, 180.0, 85.051129]), ('control #1', [1, 1, 0, 'xyz'], [0.0, 0.0, 180.0, 85.051129]), ('control #2', [2, 3, 3, 'tms'], [90.0, 66.51326, 180.0, 85.051129]), ('control #3', [3, 5, 2, 'xyz'], [45.0, 40.979898, 90.0, 66.51326]), ('control #9', [8, 255, 0, 'tms'], [178.59375, -85.051129, 180.0, -84.928321]), ('control #11', [10, 550, 688, 'tms'], [13.359375, 52.48278, 13.710938, 52.696361]), ('control #14', [1, 0, 1, 'tms'], [-180.0, 0.0, 0.0, 85.051129]), ('control #15', [9, 300, 200, 'xyz'], [30.9375, 36.031332, 31.640625, 36.597889])]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
control #0[-180.0, -85.051129, 180.0, 85.051129][-180.0, -85.051129, 180.0, 85.051129]Passed
control #1[0.0, 0.0, 180.0, 85.051129][0.0, 0.0, 180.0, 85.051129]Passed
control #2[90.0, 66.51326, 180.0, 85.051129][90.0, 66.51326, 180.0, 85.051129]Passed
control #3[45.0, 40.979898, 90.0, 66.51326][45.0, 40.979898, 90.0, 66.51326]Passed
control #4[-22.5, 21.943046, 0.0, 40.979898][-22.5, 21.943046, 0.0, 40.979898]Passed
control #5[0.0, 48.922499, 11.25, 55.776573][0.0, 48.922499, 11.25, 55.776573]Passed
control #6[5.625, -52.48278, 11.25, -48.922499][5.625, -52.48278, 11.25, -48.922499]Passed
control #9[178.59375, -85.051129, 180.0, -84.928321][178.59375, -85.051129, 180.0, -84.928321]Passed

SHA-256 / 538c0fb24c165d8b3fd11eabb99216ec4a8fcd565c80e4565e2b32150b59ead7

Verification & scope

Stipulated deterministic toy contract on a bounded input domain; results are rounded as stated and no conformance with any published standard or library is claimed. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:48:15.821667+00:00.

Case digest / 56a27217de9cd22fec82df59959cdacaf94c980e92b26b5b9724f319e9966bc2