Put text-field on the same symbol layer as the icon, with text-optional: true
text-allow-overlap: false. The result: icons always render; labels auto-hide
only where they would collide, and reappear as you zoom in. No data is lost.
map.addLayer({ id:"poi-glyph", type:"symbol", source:"pois",
layout:{
"icon-image":["get","_iconimg"], "icon-anchor":"center", "icon-allow-overlap":true,
// label centred BELOW the marker:
"text-field":["coalesce",["get","_label"],""],
"text-size":11.5, "text-anchor":"top", "text-offset":[0,0.9],
"text-justify":"center", "text-max-width":8,
"text-optional":true, // icon stays even if label is dropped
"text-allow-overlap":false // labels declutter against each other
},
paint:{ "text-color":"#3f4035", "text-halo-color":"#fff", "text-halo-width":1.6 }});
Key combination:
text-anchor:"top" + text-offset:[0, Y] → label sits centred below the marker.text-optional:true is what protects the icon — without it, a dropped label drops
the whole symbol (icon included).text-allow-overlap:false (the default) lets crowded labels declutter.map.addLayer({ id:"trail-label", type:"symbol", source:"trail",
layout:{ "symbol-placement":"line", "text-field":["coalesce",["get","Name"],""],
"text-size":12, "text-letter-spacing":0.02 },
paint:{ "text-color":"#753c1c", "text-halo-color":"#fff", "text-halo-width":2 }});
Set text-field to "" to hide, restore the expression to show — no layer rebuild:
map.setLayoutProperty("poi-glyph", "text-field",
showLabels ? ["coalesce",["get","_label"],""] : "");