I've created five GeoJSONs for H3 resolution cells from 3 to 7. Each GeoJSON was then converted into mbtile for different zoom level and joined together to form a single mbtile. Now, I can render different H3 levels at different zoom levels but when I try to query using a bounding box I can only query the layer at the current zoom level.

I want to get the values (property of the H3 cell) at higher zoom level (10) but keep the UI at the lower map zoom level (3 or 4). What are my options to achieve this, is there a way to get this from backend, my tiles are currently hosted on tileserver-gl?

Sample Layer:

    'id': 'your-layer-03',
    'type': 'fill', // or 'line' or 'symbol', depending on your tileset
    'source': 'ec-tileset',
    'source-layer': 'h3_cells_res_3', // Replace with your source layer name
    'layout': {},
    'paint': {
        'fill-color': [
            ['get', 'DAI'],
            0, '#FFFFFF',
            0.5, '#FFCCA8',
            1, '#FD6B20'
        'fill-opacity': 0.5

Query Code:

function getH3Values(inputPolygon) {
    let nonEmptyFeatures;
    let layerNames = ['your-layer-03', 'your-layer-04', 'your-layer-05', 'your-layer-06', 'your-layer-07', 'your-layer-08'];
// Loop through each layer and filter features based on the drawn polygon
for (let layerName of layerNames) {
    let features = map.queryRenderedFeatures({ layers: [layerName] });
    //var allFeatures = map.querySourceFeatures('ec-query', {sourceLayer: 'h3_cells_res_7'});
    let filteredFeatures = features.filter(function (feature) {
        if(feature.geometry.type === "Point") {
            return turf.booleanPointInPolygon(feature.geometry.coordinates, inputPolygon);
        } else if(feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
            return turf.intersect(feature.geometry, inputPolygon);
        return false;

    // If filteredFeatures is not empty, assign it to nonEmptyFeatures and break the loop
    if (filteredFeatures.length > 0) {
        nonEmptyFeatures = filteredFeatures;


// If no features are found in all layers, log a message and return
if (!nonEmptyFeatures) {
    console.log("No features found in all layers.");

var featureValues = [];

// Iterate over each feature
nonEmptyFeatures.forEach(function(feature) {



I'm trying to get data from higher zoom levels (10) while having the map shown at lower zoom (maybe 3) for entire country like USA or a bbox that is bigger than the viewport when someone zooms-in.

  • if the mbtiles are drop features at lower zoom levels(zoomed out 1,2,3 etc..) than I believe only the features you are visually seeing will be queryable. A workaround can be to load the geojson onto the map but make it completely transparent and query the geojson that way or use a middleware/backend service
    – ziggy
    Oct 6, 2023 at 21:24

2 Answers 2


You can use my tool tilequery to query vector tiles using a point or bounding box. For polygons beware of the geometry since it will be clipped to the vector tile but you can access the properties no problem. You can see it in action on this site on the esri map.


I can add more details when I have more time.


One possible solution is to create second map that's copy of the original one, but not displayed. This one can then be used to set zoom independent of the displayed map and retrieve features from there.

Code below is proof of concept. It's probably not the most efficient, but it works. It's modified Mapbox example https://docs.mapbox.com/mapbox-gl-js/example/filter-features-within-map-view/.

Click on the map produces list of the airports, but at the zoom 5, centered on the clicked point, while displayed map zoom is not changed. Bounding box of zoomed map is shown with red outline.

Here is full code:

<!DOCTYPE html>
  <meta charset="utf-8">
  <title>Filter features within map view</title>
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
  <link href="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css" rel="stylesheet">
  <script src="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js"></script>
    body { margin: 0; padding: 0; }
    #map1 {
      position: absolute;
      left: 25%;
      top: 0;
      bottom: 0;
      width: 75%;
    #map2 {
      position: absolute;
      left: 25%;
      top: 0;
      bottom: 0;
      width: 75%;
    .map-overlay {
      position: absolute;
      width: 25%;
      top: 0;
      bottom: 0;
      left: 0;
      font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
      background-color: #efefef;
      max-height: 100%;
      overflow: hidden;
    .map-overlay .listing {
      overflow: auto;
      max-height: 100%;
    .map-overlay .listing > * {
      display: block;
      padding: 5px 10px;
      margin: 0;
    .map-overlay .listing a:last-child {
      border: none;
  <div id="map1"></div> 
  <div id="map2" style="display: none;"></div>
  <div class="map-overlay">
      <div id="feature-listing" class="listing"></div>
    mapboxgl.accessToken = 'YOUR ACCESS TOKEN';
    const map1 = new mapboxgl.Map({
      container: 'map1',
      style: 'mapbox://styles/mapbox/streets-v12',
      center: [-98, 38.88],
      maxZoom: 15,
      minZoom: 1,
      zoom: 3
    const map2 = new mapboxgl.Map({
      container: 'map2',
      style: 'mapbox://styles/mapbox/streets-v12',
      center: [-98, 38.88],
      maxZoom: 15,
      minZoom: 1,
      zoom: 3
    const listingEl = document.getElementById('feature-listing');
    function renderListings(features) {
      const empty = document.createElement('p');
      listingEl.innerHTML = '';
      if (features.length) {
        for (const feature of features) {
          const item = document.createElement('div');
          const label = `${feature.properties.name} (${feature.properties.abbrev})`;
          item.innerHTML = label;
      } else {
        empty.textContent = 'No results found';
    function getUniqueFeatures(features, comparatorProperty) {
      const uniqueIds = new Set();
      const uniqueFeatures = [];
      for (const feature of features) {
        const id = feature.properties[comparatorProperty];
        if (!uniqueIds.has(id)) {
      return uniqueFeatures;
    function listFeatures(map) {
      const features = map.queryRenderedFeatures({ layers: ['airport'] });
      if (features) {
        const uniqueFeatures = getUniqueFeatures(features, 'iata_code');
    var source = {
      'type': 'vector',
      'url': 'mapbox://mapbox.04w69w5j'
    var layer = {
      'id': 'airport',
      'source': 'airports',
      'source-layer': 'ne_10m_airports',
      'type': 'circle',
      'paint': {
        'circle-color': '#4264fb',
        'circle-radius': 4,
        'circle-stroke-width': 2,
        'circle-stroke-color': '#ffffff'
    const geojson = {
      'type': 'FeatureCollection',
      'features': []
    const bbox = {
      'type': 'Feature',
      'geometry': {
        'type': 'Polygon',
        'coordinates': []
    map1.on('load', () => {
      map1.addSource('airports', source);
      map1.on('click', (evt) => {
        console.log([evt.lngLat.lng, evt.lngLat.lat]);
        map2.once('idle', function() {
          var bounds = map2.getBounds();
          var east = bounds.getEast();
          var west = bounds.getWest();
          var north = bounds.getNorth();
          var south = bounds.getSouth();
          var bboxCoords = [[west, north], [west, south], [east, south], [east, north], [west, north]];
          bbox.geometry.coordinates[0] = bboxCoords;
          center: [evt.lngLat.lng, evt.lngLat.lat],
          zoom: 5
      map1.addSource('geojson', {
        'type': 'geojson',
        'data': geojson
        id: 'bbox',
        type: 'line',
        source: 'geojson',
        layout: {
          'line-cap': 'round',
          'line-join': 'round'
        paint: {
          'line-color': '#ff0000',
          'line-width': 2
    map2.on('load', () => {
      map2.addSource('airports', source);

Example of the result upon map click:

enter image description here

  • Interesting idea, but, if I want to query features for entire country like USA, and i zoom into the higher resolution on this hidden map-2, will the query still work because the zoomed in map-2 now doesn't have full view or rendered features of the country but is only limited to the zoomed view. Oct 8, 2023 at 22:40
  • Nowhere in your question you mention that in higher zoom level you want entire USA data. Please edit your question and add that info. I'll see how to take care of that.
    – TomazicM
    Oct 9, 2023 at 9:23
  • On a second thought: if you need all the data at once, why not use the original GeoJSON?
    – TomazicM
    Oct 9, 2023 at 14:35
  • I'm not sure how will I render 5GB Geojson on the screen, because h3 10 cells are in millions, isn't vector tiling meant for this purpose? Oct 9, 2023 at 15:38
  • What I meant is to use original GeoJSON just to get desired properties, not for display on screen. It could even be modified for this purpose to be without geometries, just JSON with properties. I suppose there is some property that identifies individual cell.
    – TomazicM
    Oct 9, 2023 at 15:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.