Skip to content

Commit cd50047

Browse files
authored
Continue Overture support: POIs, Places, Airports (#579)
* Added some Oakland-specific visual test areas * Add Overture Airport Runways to Roads Layer Add Overture `theme=base / type=infrastructure / subtype=airport` runway and taxiway linestrings to the roads layer, matching OSM parity where `aeroway=runway` appears with `kind=aeroway`, `kind_detail=runway`, `min_zoom=9`. - Add `overtureAerowayKindsIndex` mapping Overture class=runway/taxiway/taxilane to kind=aeroway with appropriate kind_detail values - Extend `processOverture()` with a new branch for base/infrastructure features, emitting line geometries at min_zoom=9 (runway) or min_zoom=10 (taxiway) - Add two Overture unit tests: kind_aeroway_fromRunwayClass and kind_aeroway_fromTaxiwayClass * Fix Overture places at low zoom levels; use real Overture UUIDs in tests - Fix populationFallback in processOverture: use 0 when population > 0, so cities/towns with real population data no longer get forced into fallback zoom levels (was hardcoded to 1, causing city minZoom=8 instead of 7) - Add wikidata lookup block to processOverture mirroring the OSM path, so entries in places.csv (Q62, Q16553, Q169943, etc.) now override minZoom and populationRank for Overture locality features - Output wikidata attribute on Overture features that have it - Add failing-first tests for SF (Q62→minZoom=2), San Jose (Q16553→4), San Mateo (Q169943→6), Saratoga (Q927163→7, pop present no fallback) - Update testOaklandCity: min_zoom 9→8 (population present, no fallback) - Use real Overture UUIDs from Oakland-visualtests.parquet in all new tests * Use Overture confidence for POI filtering and rendering priority Drop features below confidence 0.65 (junk tier: ~127k features dominated by real estate listings, beauty salons, ATMs from uncertain sources). Within the remaining features, use confidence to break sort key ties so higher-confidence POIs win label collision resolution at the same zoom. Sort key: minZoom * 1000 - (int)(confidence * 100), so confidence=0.99 scores 99 points lower (higher priority) than confidence=0.65. Tests updated: websiteQid_ineligibleCategory_dropped and websiteQid_lowConfidence_dropped now correctly expect zero features. kind_nationalPark_fromBasicCategory switched to Pinnacles National Park (4d619bc0, confidence=0.917) since the previous Alcatraz fixture (814b8a78, confidence=0.639) falls below the new cutoff. Prompt: "Let's bring more Overture confidence into POI rendering: make higher-confidence POIs higher rendering priority, and simply omit ones below 0.65 (junk tier)" * Extract getZoomsPops() to remove duplicated zoom/rank logic in Places.java
1 parent bfb7fd7 commit cd50047

File tree

9 files changed

+386
-54
lines changed

9 files changed

+386
-54
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Tiles 4.14.4
2+
------
3+
- Continue expanding Overture support to lower zoom levels with Places and some infrastructure [#579]
4+
15
Tiles 4.14.3
26
------
37
- Fix OSM place selection regression at zoom=7 [#576]

app/src/examples.json

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,116 @@
11
[
2+
{
3+
"name": "oakland-downtown-z4",
4+
"description": "Area around downtown Oakland",
5+
"tags": ["oakland", "oakland-downtown"],
6+
"center": [-122.21885, 37.71324],
7+
"zoom": 4
8+
},
9+
{
10+
"name": "oakland-downtown-z5",
11+
"description": "Area around downtown Oakland",
12+
"tags": ["oakland", "oakland-downtown"],
13+
"center": [-122.21885, 37.71324],
14+
"zoom": 5
15+
},
16+
{
17+
"name": "oakland-downtown-z6",
18+
"description": "Area around downtown Oakland",
19+
"tags": ["oakland", "oakland-downtown"],
20+
"center": [-122.21885, 37.71324],
21+
"zoom": 6
22+
},
23+
{
24+
"name": "oakland-downtown-z7",
25+
"description": "Area around downtown Oakland",
26+
"tags": ["oakland", "oakland-downtown"],
27+
"center": [-122.2713, 37.8043],
28+
"zoom": 7
29+
},
30+
{
31+
"name": "oakland-downtown-z8",
32+
"description": "Area around downtown Oakland",
33+
"tags": ["oakland", "oakland-downtown"],
34+
"center": [-122.21885, 37.71324],
35+
"zoom": 8
36+
},
37+
{
38+
"name": "oakland-downtown-z9",
39+
"description": "Area around downtown Oakland",
40+
"tags": ["oakland", "oakland-downtown"],
41+
"center": [-122.2713, 37.8043],
42+
"zoom": 9
43+
},
44+
{
45+
"name": "oakland-downtown-z10",
46+
"description": "Area around downtown Oakland",
47+
"tags": ["oakland", "oakland-downtown"],
48+
"center": [-122.2713, 37.8043],
49+
"zoom": 10
50+
},
51+
{
52+
"name": "oakland-downtown-z11",
53+
"description": "Area around downtown Oakland",
54+
"tags": ["oakland", "oakland-downtown"],
55+
"center": [-122.2713, 37.8043],
56+
"zoom": 11
57+
},
58+
{
59+
"name": "oakland-airport-z15",
60+
"description": "Area around Oakland Airport terminals",
61+
"tags": ["oakland", "oakland-airport"],
62+
"center": [-122.21885, 37.71324],
63+
"zoom": 15
64+
},
65+
{
66+
"name": "oakland-airport-z14",
67+
"description": "Area around Oakland Airport terminals",
68+
"tags": ["oakland", "oakland-airport"],
69+
"center": [-122.21885, 37.71324],
70+
"zoom": 14
71+
},
72+
{
73+
"name": "oakland-airport-z13",
74+
"description": "Area around Oakland Airport terminals",
75+
"tags": ["oakland", "oakland-airport"],
76+
"center": [-122.21885, 37.71324],
77+
"zoom": 13
78+
},
79+
{
80+
"name": "oakland-airport-z12",
81+
"description": "Area around Oakland Airport terminals",
82+
"tags": ["oakland", "oakland-airport"],
83+
"center": [-122.21885, 37.71324],
84+
"zoom": 12
85+
},
86+
{
87+
"name": "oakland-lake-merritt-z15",
88+
"description": "Area around Oakland Lake Merritt",
89+
"tags": ["oakland", "oakland-lake-merritt"],
90+
"center": [-122.26467, 37.80661],
91+
"zoom": 15
92+
},
93+
{
94+
"name": "oakland-lake-merritt-z14",
95+
"description": "Area around Oakland Lake Merritt",
96+
"tags": ["oakland", "oakland-lake-merritt"],
97+
"center": [-122.26467, 37.80661],
98+
"zoom": 14
99+
},
100+
{
101+
"name": "oakland-lake-merritt-z13",
102+
"description": "Area around Oakland Lake Merritt",
103+
"tags": ["oakland", "oakland-lake-merritt"],
104+
"center": [-122.26467, 37.80661],
105+
"zoom": 13
106+
},
107+
{
108+
"name": "oakland-lake-merritt-z12",
109+
"description": "Area around Oakland Lake Merritt",
110+
"tags": ["oakland", "oakland-lake-merritt"],
111+
"center": [-122.26467, 37.80661],
112+
"zoom": 12
113+
},
2114
{
3115
"name": "taiwan-z8",
4116
"description": "Compare the appearance of highways",

tiles/src/main/java/com/protomaps/basemap/Basemap.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public String description() {
133133

134134
@Override
135135
public String version() {
136-
return "4.14.3";
136+
return "4.14.4";
137137
}
138138

139139
@Override

tiles/src/main/java/com/protomaps/basemap/layers/Places.java

Lines changed: 45 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -269,42 +269,19 @@ public void processOsm(SourceFeature sf, FeatureCollector features) {
269269
return;
270270
}
271271

272-
Integer minZoom;
273-
Integer maxZoom;
274-
Integer kindRank;
275-
276272
var computedTags = makeTagMap(kind, kindDetail, population, populationFallback);
277273
var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, computedTags);
278-
var zoomMatches = zoomsIndex.getMatches(sf2);
279274

280275
// Use populationFallback for sorting if no real population
281276
if (population == 0 && populationFallback > 0) {
282277
population = populationFallback;
283278
}
284279

285-
minZoom = getInteger(sf2, zoomMatches, "pm:minzoom", 99);
286-
maxZoom = getInteger(sf2, zoomMatches, "pm:maxzoom", 99);
287-
kindRank = getInteger(sf2, zoomMatches, "pm:kindRank", 99);
288-
289-
int populationRank = 0;
290-
291-
for (int i = 0; i < popBreaks.length; i++) {
292-
if (population >= popBreaks[i]) {
293-
populationRank = i + 1;
294-
}
295-
}
296-
297-
if (WIKIDATA_CONFIGS.containsKey(sf.getString("wikidata"))) {
298-
var wikidataConfig = WIKIDATA_CONFIGS.get(sf.getString("wikidata"));
299-
if (kind.equals("country") || kind.equals("region")) {
300-
minZoom = wikidataConfig.minZoom();
301-
maxZoom = wikidataConfig.maxZoom();
302-
}
303-
if (kind.equals("locality")) {
304-
minZoom = wikidataConfig.minZoom();
305-
populationRank = wikidataConfig.rankMax();
306-
}
307-
}
280+
var zp = getZoomsPops(sf2, kind, population);
281+
int minZoom = zp.minZoom();
282+
int maxZoom = zp.maxZoom();
283+
int kindRank = zp.kindRank();
284+
int populationRank = zp.populationRank();
308285

309286
var feat = features.point(this.name())
310287
.setId(FeatureId.create(sf))
@@ -379,32 +356,19 @@ public void processOverture(SourceFeature sf, FeatureCollector features) {
379356
}
380357
}
381358

382-
// Overture always uses populationFallback for zoom calculations to get consistent behavior
383-
// This ensures Overture places get the higher minzoom levels (8 for city, 9 for town, etc)
384-
Integer populationFallback = 1; // Marker value to trigger fallback zoom levels
385-
386-
Integer minZoom;
387-
Integer maxZoom;
388-
Integer kindRank;
359+
Integer populationFallback = (population > 0) ? 0 : 1;
389360

390361
var computedTags = makeTagMap(kind, kindDetail, population, populationFallback);
391362
var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, computedTags);
392-
var zoomMatches = zoomsIndex.getMatches(sf2);
393-
394-
minZoom = getInteger(sf2, zoomMatches, "pm:minzoom", 99);
395-
maxZoom = getInteger(sf2, zoomMatches, "pm:maxzoom", 99);
396-
kindRank = getInteger(sf2, zoomMatches, "pm:kindRank", 99);
397363

398364
// Extract name
399365
String name = sf.getString("names.primary");
400366

401-
int populationRank = 0;
402-
403-
for (int i = 0; i < popBreaks.length; i++) {
404-
if (population >= popBreaks[i]) {
405-
populationRank = i + 1;
406-
}
407-
}
367+
var zp = getZoomsPops(sf2, kind, population);
368+
int minZoom = zp.minZoom();
369+
int maxZoom = zp.maxZoom();
370+
int kindRank = zp.kindRank();
371+
int populationRank = zp.populationRank();
408372

409373
var feat = features.point(this.name())
410374
.setAttr("kind", kind)
@@ -418,6 +382,10 @@ public void processOverture(SourceFeature sf, FeatureCollector features) {
418382
feat.setAttr("kind_detail", kindDetail);
419383
}
420384

385+
if (sf.hasTag("wikidata")) {
386+
feat.setAttr("wikidata", sf.getString("wikidata"));
387+
}
388+
421389
int sortKey = getSortKey(minZoom, kindRank, population, name);
422390
feat.setSortKey(sortKey);
423391
feat.setAttr("sort_key", sortKey);
@@ -428,6 +396,36 @@ public void processOverture(SourceFeature sf, FeatureCollector features) {
428396
feat.setBufferPixelOverrides(ZoomFunction.maxZoom(12, 64));
429397
}
430398

399+
record ZoomsPops(int minZoom, int maxZoom, int kindRank, int populationRank) {}
400+
401+
private ZoomsPops getZoomsPops(Matcher.SourceFeatureWithComputedTags sf2, String kind, int population) {
402+
var zoomMatches = zoomsIndex.getMatches(sf2);
403+
int minZoom = getInteger(sf2, zoomMatches, "pm:minzoom", 99);
404+
int maxZoom = getInteger(sf2, zoomMatches, "pm:maxzoom", 99);
405+
int kindRank = getInteger(sf2, zoomMatches, "pm:kindRank", 99);
406+
407+
int populationRank = 0;
408+
for (int i = 0; i < popBreaks.length; i++) {
409+
if (population >= popBreaks[i]) {
410+
populationRank = i + 1;
411+
}
412+
}
413+
414+
if (WIKIDATA_CONFIGS.containsKey(sf2.getString("wikidata"))) {
415+
var wikidataConfig = WIKIDATA_CONFIGS.get(sf2.getString("wikidata"));
416+
if (kind.equals("country") || kind.equals("region")) {
417+
minZoom = wikidataConfig.minZoom();
418+
maxZoom = wikidataConfig.maxZoom();
419+
}
420+
if (kind.equals("locality")) {
421+
minZoom = wikidataConfig.minZoom();
422+
populationRank = wikidataConfig.rankMax();
423+
}
424+
}
425+
426+
return new ZoomsPops(minZoom, maxZoom, kindRank, populationRank);
427+
}
428+
431429
@Override
432430
public List<VectorTile.Feature> postProcess(int zoom, List<VectorTile.Feature> items) {
433431
return items;

tiles/src/main/java/com/protomaps/basemap/layers/Pois.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,13 @@ public void processOverture(SourceFeature sf, FeatureCollector features) {
562562
if (kind.equals("pm:undefined"))
563563
return;
564564

565+
// Drop low-confidence features. Below 0.65, features are dominated by uncertain data:
566+
// real estate listings, auto repair, beauty salons, ATMs from low-quality sources.
567+
double confidence = sf.getTag("confidence")instanceof Number n ? n.doubleValue() : 0.0;
568+
if (confidence < 0.65) {
569+
return;
570+
}
571+
565572
// QRank may override minZoom entirely
566573
String wikidata = sf.getString("wikidata");
567574
long qrank = (wikidata != null) ? qrankDb.get(wikidata) : 0;
@@ -581,6 +588,10 @@ public void processOverture(SourceFeature sf, FeatureCollector features) {
581588

582589
String name = sf.getString("names.primary");
583590

591+
// Sort key: lower = higher rendering priority. Within the same minZoom bucket,
592+
// higher confidence wins (subtract confidence*100 so 0.99 → -99, 0.65 → -65).
593+
int sortKey = minZoom * 1000 - (int) (confidence * 100);
594+
584595
features.point(this.name())
585596
// all POIs should receive their IDs at all zooms
586597
// (there is no merging of POIs like with lines and polygons in other layers)
@@ -593,7 +604,8 @@ public void processOverture(SourceFeature sf, FeatureCollector features) {
593604
.setAttr("min_zoom", minZoom + 1)
594605
//
595606
.setBufferPixels(8)
596-
.setZoomRange(Math.min(minZoom, 15), 15);
607+
.setZoomRange(Math.min(minZoom, 15), 15)
608+
.setSortKey(sortKey);
597609
}
598610

599611
@Override

tiles/src/main/java/com/protomaps/basemap/layers/Roads.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,17 @@ public Roads(CountryCoder countryCoder) {
219219

220220
)).index();
221221

222+
private static final MultiExpression.Index<Map<String, Object>> overtureAerowayKindsIndex =
223+
MultiExpression.ofOrdered(List.of(
224+
rule(use("pm:kind", "pm:undefined"), use("pm:kindDetail", "pm:undefined"), use("pm:highway", "pm:undefined")),
225+
rule(with("class", "runway"), use("pm:kind", "aeroway"), use("pm:kindDetail", "runway"),
226+
use("pm:highway", "aeroway")),
227+
rule(with("class", "taxiway"), use("pm:kind", "aeroway"), use("pm:kindDetail", "taxiway"),
228+
use("pm:highway", "aeroway")),
229+
rule(with("class", "taxilane"), use("pm:kind", "aeroway"), use("pm:kindDetail", "taxiway"),
230+
use("pm:highway", "aeroway"))
231+
)).index();
232+
222233
// Protomaps kind/kind_detail to min_zoom mapping
223234

224235
private static final MultiExpression.Index<Map<String, Object>> highwayZoomsIndex = MultiExpression.ofOrdered(List.of(
@@ -505,6 +516,30 @@ private static class OvertureSegmentProperties {
505516
}
506517

507518
public void processOverture(SourceFeature sf, FeatureCollector features) {
519+
if ("base".equals(sf.getString("theme")) && "infrastructure".equals(sf.getString("type"))) {
520+
if (!"airport".equals(sf.getString("subtype")))
521+
return;
522+
523+
List<Map<String, Object>> kindMatches = overtureAerowayKindsIndex.getMatches(sf);
524+
String kind = getString(sf, kindMatches, "pm:kind", "pm:undefined");
525+
String kindDetail = getString(sf, kindMatches, "pm:kindDetail", "pm:undefined");
526+
if ("pm:undefined".equals(kind))
527+
return;
528+
529+
int minZoom = "runway".equals(kindDetail) ? 9 : 10;
530+
String name = sf.getString("names.primary");
531+
532+
if (!sf.canBePolygon()) {
533+
try {
534+
LineString line = (LineString) sf.latLonGeometry();
535+
emitOvertureFeature(features, sf, line, kind, kindDetail, name, "aeroway", minZoom,
536+
new OvertureSegmentProperties());
537+
} catch (GeometryException e) {
538+
/* skip */ }
539+
}
540+
return;
541+
}
542+
508543
// Filter by type field - Overture transportation theme
509544
if (!"transportation".equals(sf.getString("theme"))) {
510545
return;

0 commit comments

Comments
 (0)