Repository: ndsev/mapget (language: python)

Issue:
Python Data Source Library 
Use `pybind11` to create the `mapget` python package, which should initially include only bindings for the `mapget-model` and `mapget-datasource` libraries.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/cmake.yaml b/.github/workflows/cmake.yaml
index 0dea66c1..ac25e282 100644
--- a/.github/workflows/cmake.yaml
+++ b/.github/workflows/cmake.yaml
@@ -1,4 +1,4 @@
-name: Build
+name: CI
 
 on:
   push:
@@ -7,140 +7,84 @@ on:
     branches: [ '**' ]
 
 jobs:
-  build-linux:
-    name: Linux
+  build-manylinux:
+    strategy:
+      matrix:
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
     runs-on: ubuntu-latest
+    container: ghcr.io/klebert-engineering/manylinux-cpp17-py${{ matrix.python-version }}:2023.1
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
         with:
-          submodules: true
-      - name: Install dependencies
-        run: |
-          sudo apt-get install gcovr ninja-build valgrind
-      - name: Setup
-        run: |
-          mkdir build
+          submodules: recursive
       - name: Configure
-        working-directory: build
         run: |
-          cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=YES -GNinja ..
+          python3 -m venv venv && . ./venv/bin/activate
+          pip install -U setuptools wheel pip
+          mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release ..
       - name: Build
         working-directory: build
         run: |
+          . ../venv/bin/activate
           cmake --build .
-
-      - name: Run CTest
+          mv bin/wheel bin/wheel-auditme
+          auditwheel repair bin/wheel-auditme/mapget*.whl -w bin/wheel
+      - name: Test
         working-directory: build
         run: |
-          ctest --verbose --output-junit result-junit.xml
-      # - name: Publish Test Results
-      #   uses: EnricoMi/publish-unit-test-result-action@v2
-      #   id: test-results
-      #   if: always()
-      #   with:
-      #     files: |
-      #       build/test/*.xml
-      # - name: Set badge color
-      #   shell: bash
-      #   run: |
-      #     case ${{ fromJSON( steps.test-results.outputs.json ).conclusion }} in
-      #       success)
-      #         echo "BADGE_COLOR=31c653" >> $GITHUB_ENV
-      #         ;;
-      #       failure)
-      #         echo "BADGE_COLOR=800000" >> $GITHUB_ENV
-      #         ;;
-      #       neutral)
-      #         echo "BADGE_COLOR=696969" >> $GITHUB_ENV
-      #         ;;
-      #     esac
-      # - name: Create badge
-      #   uses: emibcn/badge-action@d6f51ff11b5c3382b3b88689ae2d6db22d9737d1
-      #   with:
-      #     label: Tests
-      #     status: '${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.tests }} tests, ${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs }} runs: ${{ fromJSON( steps.test-results.outputs.json ).conclusion }}'
-      #     color: ${{ env.BADGE_COLOR }}
-      #     path: badge.svg
-      # - name: Upload badge to Gist
-      #   # Upload only for main branch
-      #   if: >
-      #     github.event_name == 'workflow_run' && github.event.workflow_run.head_branch == 'main' ||
-      #     github.event_name != 'workflow_run' && github.ref == 'refs/heads/main'
-      #   uses: andymckay/append-gist-action@1fbfbbce708a39bd45846f0955ed5521f2099c6d
-      #   with:
-      #     token: ${{ secrets.GIST_TOKEN }}
-      #     gistURL: https://gist.githubusercontent.com/johannes-wolf/61e57af50757b03e0c7cd119ec2d2f4b
-      #     file: badge.svg
-
-      - name: Run Valgrind Memcheck
-        working-directory: build/bin
-        run: |
-          valgrind --tool=memcheck ./test.mapget
-      - name: Run Valgrind Helgrind
-        working-directory: build/bin
-        run: |
-          valgrind --tool=helgrind ./test.mapget
-      - name: Run Valgrind Cachegrind
-        working-directory: build/bin
-        run: |
-          valgrind --tool=cachegrind ./test.mapget | tee cachegrind-out.txt
-      - name: Run Valgrind Callgrind
-        working-directory: build/bin
-        run: |
-          valgrind --tool=callgrind --instr-atstart=no ./test.mapget
-      - name: Publish Callgrind Output
-        uses: actions/upload-artifact@v3
+          . ../venv/bin/activate
+          ctest -C Release --verbose --no-test=fail
+      - name: Deploy
+        uses: actions/upload-artifact@v2
         with:
-          name: Callgrind Output
-          path: build/bin/callgrind.out.*
-
-  build-windows:
-    name: Windows
-    runs-on: windows-latest
+          name: mapget-py${{ matrix.python-version }}-ubuntu-latest
+          path: build/bin/wheel/*.whl
+  build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [macos-latest, windows-latest]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
         with:
-          submodules: true
-      - name: Setup
-        run: |
-          mkdir build
-      - name: Configure
-        working-directory: build
-        run: |
-          cmake ..
-        shell: cmd
-      - name: Build
-        working-directory: build
-        run: |
-          cmake --build .
-        shell: cmd
-      - name: Run CTest
-        working-directory: build/test
-        run: |
-          ctest -C Debug --verbose
-        shell: cmd
-
-  build-macos:
-    name: MacOS
-    runs-on: macos-latest
-    steps:
-      - uses: actions/checkout@v3
+          submodules: recursive
+      - uses: actions/setup-python@v2
         with:
-          submodules: true
-      - name: Setup
-        run: |
-          brew install openssl        
-          mkdir build
-      - name: Configure
+          python-version: ${{ matrix.python-version }}
+          architecture: x64
+      - run: python -m pip install setuptools wheel
+      - run: mkdir build
+      - name: Build (macOS)
+        if: matrix.os == 'macos-latest' || matrix.os == 'macos-10.15'
         working-directory: build
         run: |
+          python -m pip install delocate
+          brew install openssl
           export OPENSSL_ROOT_DIR=$(brew --prefix openssl)
-          cmake ..
-      - name: Build
+          cmake -DPython3_ROOT_DIR=$pythonLocation \
+                -DPython3_FIND_FRAMEWORK=LAST \
+                -DCMAKE_BUILD_TYPE=Release \
+                -DHTTPLIB_USE_BROTLI_IF_AVAILABLE=OFF ..
+          cmake --build .
+          mv bin/wheel bin/wheel-auditme  # Same as on Linux
+          ./_deps/python-cmake-wheel-src/repair-wheel-macos.bash \
+                "$(pwd)"/bin/wheel-auditme/mapget*.whl \
+                "$(pwd)"/bin/wheel mapget
+      - name: Build (Windows)
+        if: matrix.os == 'windows-latest'
         working-directory: build
         run: |
-          cmake --build .
-      - name: Run CTest
-        working-directory: build/test
+          choco install --no-progress -y openssl
+          echo "cmake -DPython3_ROOT_DIR=$env:pythonLocation"
+          cmake "-DPython3_ROOT_DIR=$env:pythonLocation" -DPython3_FIND_REGISTRY=LAST -DHTTPLIB_USE_ZLIB_IF_AVAILABLE=OFF -DCMAKE_BUILD_TYPE=Release ..
+          cmake --build . --config Release
+      - name: Deploy
+        uses: actions/upload-artifact@v2
+        with:
+          name: mapget-py${{ matrix.python-version }}-${{ matrix.os }}
+          path: build/bin/wheel/*.whl
+      - name: Test
+        working-directory: build
         run: |
-          ctest --verbose
+          ctest -C Release --verbose --no-test=fail
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
new file mode 100644
index 00000000..63fa59e1
--- /dev/null
+++ b/.github/workflows/deploy.yaml
@@ -0,0 +1,42 @@
+name: Deploy
+
+on:
+  workflow_dispatch: {}
+
+jobs:
+  deploy:
+    strategy:
+      matrix:
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        os: [macos-latest, ubuntu-latest, windows-latest]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Fetch
+        # Use official download-artifact action once fixed:
+        # https://github.com/actions/download-artifact/issues/3
+        uses: dawidd6/action-download-artifact@v2
+        with:
+          workflow: cmake.yaml
+          name: mapget-py${{ matrix.python-version }}-${{ matrix.os }}
+          path: dist/
+      - name: Upload
+        env:
+          KE_PYPI_PW: ${{ secrets.KE_PYPI_PW }}
+        run: |
+          echo "Initial state:"
+          echo "=============="
+          cd dist/
+          ls .
+          echo "Uploading..."
+          echo "============"
+          python3 -m venv venv
+          . venv/bin/activate
+          pip install twine
+          twine upload \
+            --non-interactive \
+            --skip-existing \
+            --verbose \
+            --disable-progress-bar \
+            -u klebert-engineering \
+            -p "${KE_PYPI_PW}" \
+            *.whl
diff --git a/.gitignore b/.gitignore
index bdda9def..5e965748 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ cmake-build-debug/
 cmake-build-release/
 .clang-format
 .idea/
+venv/
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 854e6f77..5aadf744 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,17 +1,23 @@
 cmake_minimum_required(VERSION 3.14)
 
-if (NOT PROJECT_NAME)
+option(MAPGET_BUILD_WHEEL "Enable mapget Python wheel (output to WHEEL_DEPLOY_DIRECTORY." OFF)
+option(MAPGET_BUILD_DATASOURCE "Enable mapget-datasource library." OFF)
+option(MAPGET_BUILD_SERVICE "Enable mapget-cache library and mapget-service app." OFF)
+
+if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
   set (MAPGET_ENABLE_TESTING ON)
+  set (MAPGET_BUILD_WHEEL ON)
+  set (MAPGET_BUILD_DATASOURCE ON)
+  set (MAPGET_BUILD_SERVICE ON)
 endif()
 
 project(mapget)
 include(FetchContent)
 
+set(MAPGET_VERSION 0.0.2)
+
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
-set(MAPGET_VERSION 0.0.1)
-
-option(MAPGET_BUILD_WHEEL "Enable mapget whl-output to WHEEL_DEPLOY_DIRECTORY." ON)
 
 if (NOT MSVC)
   set(CMAKE_POSITION_INDEPENDENT_CODE ON)
@@ -41,17 +47,6 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/cmake-modules")
 ##############
 # deps
 
-find_package(OpenSSL REQUIRED)
-
-# if (MAPGET_BUILD_WHEEL)
-#   if (NOT TARGET wheel)
-#     set(Python3_FIND_STRATEGY LOCATION)
-#     set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps/python-cmake-wheel")
-#     include(python-wheel)
-#     set(WHEEL_DEPLOY_DIRECTORY "${MAPGET_DEPLOY_DIR}/wheel")
-#   endif()
-# endif()
-
 if (NOT TARGET stx)
   FetchContent_Declare(stx
     GIT_REPOSITORY "https://github.com/Klebert-Engineering/stx.git"
@@ -77,18 +72,6 @@ if (NOT TARGET simfil)
   FetchContent_MakeAvailable(simfil)
 endif()
 
-if (NOT TARGET httplib)
-  FetchContent_Declare(httplib
-    GIT_REPOSITORY "https://github.com/yhirose/cpp-httplib.git"
-    GIT_TAG        "master"
-    GIT_SHALLOW    ON)
-  FetchContent_MakeAvailable(httplib)
-  target_compile_definitions(httplib
-    INTERFACE
-      CPPHTTPLIB_OPENSSL_SUPPORT)
-  target_link_libraries(httplib INTERFACE OpenSSL::SSL)
-endif()
-
 if (NOT TARGET bitsery)
   FetchContent_Declare(bitsery
     GIT_REPOSITORY "https://github.com/fraillt/bitsery.git"
@@ -97,12 +80,50 @@ if (NOT TARGET bitsery)
   FetchContent_MakeAvailable(bitsery)
 endif()
 
-if (NOT TARGET yaml-cpp)
-  FetchContent_Declare(yaml-cpp
-    GIT_REPOSITORY "https://github.com/jbeder/yaml-cpp.git"
-    GIT_TAG        "master"
-    GIT_SHALLOW    ON)
-  FetchContent_MakeAvailable(yaml-cpp)
+if (MAPGET_BUILD_DATASOURCE OR MAPGET_BUILD_SERVICE)
+  if (NOT TARGET httplib)
+    FetchContent_Declare(httplib
+      GIT_REPOSITORY "https://github.com/yhirose/cpp-httplib.git"
+      GIT_TAG        "master"
+      GIT_SHALLOW    ON)
+    FetchContent_MakeAvailable(httplib)
+    find_package(OpenSSL REQUIRED)
+    target_compile_definitions(httplib
+      INTERFACE
+        CPPHTTPLIB_OPENSSL_SUPPORT)
+    target_link_libraries(httplib INTERFACE OpenSSL::SSL)
+  endif()
+
+  if (NOT TARGET yaml-cpp)
+    FetchContent_Declare(yaml-cpp
+      GIT_REPOSITORY "https://github.com/jbeder/yaml-cpp.git"
+      GIT_TAG        "master"
+      GIT_SHALLOW    ON)
+    FetchContent_MakeAvailable(yaml-cpp)
+  endif()
+endif()
+
+if (MAPGET_BUILD_WHEEL)
+  if (NOT TARGET wheel)
+    set(Python3_FIND_STRATEGY LOCATION)
+    FetchContent_Declare(python-cmake-wheel
+      GIT_REPOSITORY "https://github.com/klebert-engineering/python-cmake-wheel.git"
+      GIT_TAG        "main"
+      GIT_SHALLOW    ON)
+    FetchContent_MakeAvailable(python-cmake-wheel)
+    list(APPEND CMAKE_MODULE_PATH "${python-cmake-wheel_SOURCE_DIR}")
+    include(python-wheel)
+    set(WHEEL_DEPLOY_DIRECTORY "${MAPGET_DEPLOY_DIR}/wheel")
+  endif()
+
+  if (NOT TARGET pybind11)
+    FetchContent_Declare(
+      pybind11
+      GIT_REPOSITORY https://github.com/pybind/pybind11.git
+      GIT_TAG        v2.10.4
+      GIT_SHALLOW    ON)
+    FetchContent_MakeAvailable(pybind11)
+  endif()
 endif()
 
 if (WITH_COVERAGE)
@@ -113,7 +134,14 @@ endif()
 # libs
 
 add_subdirectory(libs/model)
-add_subdirectory(libs/datasource)
+
+if (MAPGET_BUILD_DATASOURCE)
+  add_subdirectory(libs/datasource)
+endif()
+
+if (MAPGET_BUILD_WHEEL)
+  add_subdirectory(libs/pymapget)
+endif()
 
 ##############
 # tests
@@ -130,10 +158,3 @@ if (MAPGET_ENABLE_TESTING)
       EXCLUDE "*catch2*")
   endif()
 endif()
-
-##############
-# wheels
-
-# if (MAPGET_BUILD_WHEEL)
-#   add_subdirectory(wheel)
-# endif()
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..24220b6b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2022, NDS e.V. Association
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/examples/python/datasource.py b/examples/python/datasource.py
new file mode 100644
index 00000000..5714d269
--- /dev/null
+++ b/examples/python/datasource.py
@@ -0,0 +1,83 @@
+import mapget
+from mapget import Point as Pt
+
+
+def handle_tile_request(tile: mapget.TileFeatureLayer):
+    # Read out requested tile-id / map-id / layer-id
+    print(f"Got request for tile={tile.tile_id().value:02X}, map={tile.map_id()}, layer={tile.layer_id()}.")
+
+    # Create a new feature
+    feature: mapget.Feature = tile.new_feature("Way", [("wayId", 0)])
+
+    # Assign geometry (low-level)
+    geom: mapget.Geometry = feature.geom().new_geometry(mapget.GeomType.LINE)
+    geom.append(41., 11.)
+    geom.append(Pt(x=42., y=12., z=506))
+
+    # Assign geometry (high-level)
+    feature.add_point(Pt(42.5, 11.6))
+    feature.add_points([Pt(42.5, 11.6), Pt(42.5, 11.8)])
+    feature.add_line([Pt(42.5, 11.6), Pt(42.5, 11.7)])
+    feature.add_mesh([Pt(42.5, 11.6), Pt(42.5, 11.7), Pt(42.2, 11.7)])
+    feature.add_poly([Pt(42.5, 11.6), Pt(42.5, 11.7), Pt(42.2, 11.7), Pt(42.2, 11.3)])
+
+    # Add an attribute
+    fixed_attrs: mapget.Object = feature.attributes()
+    fixed_attrs.add_field("isBridge", False)
+
+    # Add an attribute which has a compound value (low-level)
+    attr_obj = tile.new_object()
+    attr_obj.add_field("name", "Main St.")
+    attr_obj.add_field("houseNumber", 5)
+    attr_arr = tile.new_array()
+    attr_arr.append(attr_obj)
+    attr_arr.append(attr_obj)
+    fixed_attrs.add_field("addresses", attr_arr)
+
+    # Add an attribute which has a compound value (high-level)
+    # Note: Map values may also be feature IDs to create references.
+    fixed_attrs.add_field("pois", [
+        {"name": "Bakery", "rating": 10},
+        {"name": "Gas Station", "rating": 8},
+        {"reference": tile.new_feature_id("Way", [("wayId", 7)])}
+    ])
+
+    # Add an attribute layer
+    attr_layer: mapget.Object = feature.attribute_layers().new_layer("rules")
+    attr: mapget.Attribute = attr_layer.new_attribute("SPEED_LIMIT")
+    attr.set_direction(mapget.Direction.POSITIVE)
+    attr.add_field("speedLimit", 50)
+
+    # Add a child feature ID
+    feature.children().append(tile.new_feature_id("Way", [("wayId", 10)]))
+
+
+# Instantiate a data source with a minimal mandatory set
+# of meta-information.
+ds = mapget.DataSourceServer({
+    "layers": {
+        "WayLayer": {
+            "featureTypes": [
+                {
+                    "name": "Way",
+                    "uniqueIdCompositions": [[{
+                        "partId": "wayId",
+                        "datatype": "I64"
+                    }]]
+                }
+            ]
+        }
+    }, "mapId": "TestMap"
+})
+
+# Set the callback which is invoked when a tile is requested.
+ds.on_tile_request(handle_tile_request)
+
+# Run the data source - you may also set port=0 to select a
+# port automatically.
+ds.go(port=54545)
+
+# Wait until Ctrl-C is hit. Navigate e.g. to
+#  http://localhost:54545/tile?layer=WayLayer&tileId=2&responseType=json
+# to test the running data source.
+ds.wait_for_signal()
diff --git a/libs/datasource/include/mapget/datasource/server.h b/libs/datasource/include/mapget/datasource/server.h
index 4fae2fe0..b73ba3b3 100644
--- a/libs/datasource/include/mapget/datasource/server.h
+++ b/libs/datasource/include/mapget/datasource/server.h
@@ -31,7 +31,7 @@ class DataSourceServer
      * If an error occurs while filling the tile, the callback can use
      * TileFeatureLayer::setError(...) to singal the error downstream.
      */
-    DataSourceServer& onTileRequest(std::function<void(TileFeatureLayer&)> const&);
+    DataSourceServer& onTileRequest(std::function<void(TileFeatureLayer::Ptr)> const&);
 
     /**
      * Launch the DataSource server in it's own thread.
diff --git a/libs/datasource/src/server.cpp b/libs/datasource/src/server.cpp
index fd05f7d3..144e66a6 100644
--- a/libs/datasource/src/server.cpp
+++ b/libs/datasource/src/server.cpp
@@ -17,7 +17,7 @@ struct DataSourceServer::Impl
 {
     httplib::Server server_;
     DataSourceInfo info_;
-    std::function<void(TileFeatureLayer&)> tileCallback_;
+    std::function<void(TileFeatureLayer::Ptr)> tileCallback_;
     uint16_t port_ = 0;
     std::thread serverThread_;
     std::shared_ptr<Fields> fields_;
@@ -40,6 +40,10 @@ struct DataSourceServer::Impl
                     fieldsOffsetParam = (simfil::FieldId)
                         std::stoul(req.get_param_value("fieldsOffset"));
 
+                std::string responseType = "binary";
+                if (req.has_param("responseType"))
+                    responseType = req.get_param_value("responseType");
+
                 auto tileFeatureLayer = std::make_shared<TileFeatureLayer>(
                     tileIdParam,
                     info_.nodeId_,
@@ -47,17 +51,22 @@ struct DataSourceServer::Impl
                     layerIt->second,
                     fields_);
                 if (tileCallback_)
-                    tileCallback_(*tileFeatureLayer);
+                    tileCallback_(tileFeatureLayer);
 
                 // Serialize TileFeatureLayer using TileLayerStream
-                std::stringstream content;
-                TileLayerStream::FieldOffsetMap fieldOffsets{{info_.nodeId_, fieldsOffsetParam}};
-                TileLayerStream::Writer layerWriter{
-                    [&](auto&& msg) { content << msg; },
-                    fieldOffsets};
-                layerWriter.write(tileFeatureLayer);
-
-                res.set_content(content.str(), "application/binary");
+                if (responseType == "binary") {
+                    std::stringstream content;
+                    TileLayerStream::FieldOffsetMap fieldOffsets{
+                        {info_.nodeId_, fieldsOffsetParam}};
+                    TileLayerStream::Writer layerWriter{
+                        [&](auto&& msg) { content << msg; },
+                        fieldOffsets};
+                    layerWriter.write(tileFeatureLayer);
+                    res.set_content(content.str(), "application/binary");
+                }
+                else {
+                    res.set_content(nlohmann::to_string(tileFeatureLayer->toGeoJson()), "application/json");
+                }
             });
 
         // Set up GET /info endpoint
@@ -92,7 +101,7 @@ DataSourceServer::DataSourceServer(DataSourceInfo const& info)
 }
 
 DataSourceServer&
-DataSourceServer::onTileRequest(std::function<void(TileFeatureLayer&)> const& callback)
+DataSourceServer::onTileRequest(std::function<void(TileFeatureLayer::Ptr)> const& callback)
 {
     impl_->tileCallback_ = callback;
     return *this;
diff --git a/libs/model/include/mapget/model/attrlayer.h b/libs/model/include/mapget/model/attrlayer.h
index df298ea8..877a2e85 100644
--- a/libs/model/include/mapget/model/attrlayer.h
+++ b/libs/model/include/mapget/model/attrlayer.h
@@ -18,6 +18,7 @@ class AttributeLayer : protected simfil::Object
     friend class TileFeatureLayer;
     friend class bitsery::Access;
     friend struct simfil::shared_model_ptr<simfil::ModelNode>;
+    friend ModelNode::Ptr;
 
 public:
     /**
@@ -45,6 +46,7 @@ class AttributeLayerList : protected simfil::Object
     friend class TileFeatureLayer;
     friend class bitsery::Access;
     friend class Feature;
+    friend ModelNode::Ptr;
 
 public:
     /**
diff --git a/libs/model/include/mapget/model/feature.h b/libs/model/include/mapget/model/feature.h
index 412f3989..429fe9f9 100644
--- a/libs/model/include/mapget/model/feature.h
+++ b/libs/model/include/mapget/model/feature.h
@@ -1,8 +1,9 @@
 #pragma once
 
-#include "featureid.h"
-#include "attrlayer.h"
 #include "attr.h"
+#include "attrlayer.h"
+#include "featureid.h"
+#include "tileid.h"
 
 #include "nlohmann/json.hpp"
 
@@ -41,28 +42,57 @@ class Feature : protected simfil::MandatoryDerivedModelNodeBase<TileFeatureLayer
 {
     friend class bitsery::Access;
     friend class TileFeatureLayer;
+    friend ModelNode::Ptr;
 
 public:
+    /** Get the name of this feature's type. */
     [[nodiscard]] std::string_view typeId() const;
 
+    /** Get this feature's ID. */
     [[nodiscard]] model_ptr<FeatureId> id() const;
+
+    /** Get this feature's GeometryCollection. */
     [[nodiscard]] model_ptr<GeometryCollection> geom();
+
+    /** Get this feature's Attribute layers. */
     [[nodiscard]] model_ptr<AttributeLayerList> attributeLayers();
+
+    /** Get this feature's un-layered attributes. */
     [[nodiscard]] model_ptr<Object> attributes();
+
+    /** Get this feature's child ID list. */
     [[nodiscard]] model_ptr<Array> children();
 
+    /** Add a point to the feature. */
+    void addPoint(Point const& p);
+
+    /** Add multiple points to the feature. */
+    void addPoints(std::vector<Point> const& points);
+
+    /** Add a line to the feature. */
+    void addLine(std::vector<Point> const& points);
+
+    /** Add a mesh to the feature. Points must be a multiple of 3. */
+    void addMesh(std::vector<Point> const& points);
+
+    /** Add a polygon to the feature. Will be auto-closed. Must not have holes. */
+    void addPoly(std::vector<Point> const& points);
+
     /**
-     * Evaluate a filter expression on this feature.
+     * Evaluate a filter expression on this feature, get the first (or Null) result.
      */
-    std::vector<simfil::Value> evaluateAll(std::string_view const& expression);
     simfil::Value evaluate(std::string_view const& expression);
 
+    /**
+     * Evaluate a filter expression on this feature, get all (or no) results.
+     */
+    std::vector<simfil::Value> evaluateAll(std::string_view const& expression);
+
     /**
      * Convert the Feature to GeoJSON.
      */
     nlohmann::json toGeoJson();
 
-
 protected:
     /**
      * Simfil Model-Node Functions
@@ -70,7 +100,7 @@ class Feature : protected simfil::MandatoryDerivedModelNodeBase<TileFeatureLayer
     [[nodiscard]] simfil::ValueType type() const override;
     [[nodiscard]] ModelNode::Ptr at(int64_t) const override;
     [[nodiscard]] uint32_t size() const override;
-    [[nodiscard]] ModelNode::Ptr get(const simfil::FieldId &) const override;
+    [[nodiscard]] ModelNode::Ptr get(const simfil::FieldId&) const override;
     [[nodiscard]] simfil::FieldId keyAt(int64_t) const override;
     [[nodiscard]] bool iterate(IterCallback const& cb) const override;
 
@@ -85,8 +115,9 @@ class Feature : protected simfil::MandatoryDerivedModelNodeBase<TileFeatureLayer
         simfil::ModelNodeAddress attrs_;
         simfil::ModelNodeAddress children_;
 
-        template<typename S>
-        void serialize(S& s) {
+        template <typename S>
+        void serialize(S& s)
+        {
             s.value4b(id_.value_);
             s.value4b(geom_.value_);
             s.value4b(attrLayers_.value_);
@@ -112,7 +143,7 @@ class Feature : protected simfil::MandatoryDerivedModelNodeBase<TileFeatureLayer
         [[nodiscard]] simfil::ValueType type() const override;
         [[nodiscard]] ModelNode::Ptr at(int64_t) const override;
         [[nodiscard]] uint32_t size() const override;
-        [[nodiscard]] ModelNode::Ptr get(const simfil::FieldId &) const override;
+        [[nodiscard]] ModelNode::Ptr get(const simfil::FieldId&) const override;
         [[nodiscard]] simfil::FieldId keyAt(int64_t) const override;
         [[nodiscard]] bool iterate(IterCallback const& cb) const override;
 
@@ -123,4 +154,4 @@ class Feature : protected simfil::MandatoryDerivedModelNodeBase<TileFeatureLayer
     };
 };
 
-}
\ No newline at end of file
+}  // namespace mapget
\ No newline at end of file
diff --git a/libs/model/include/mapget/model/featureid.h b/libs/model/include/mapget/model/featureid.h
index f993c27f..08c373f6 100644
--- a/libs/model/include/mapget/model/featureid.h
+++ b/libs/model/include/mapget/model/featureid.h
@@ -26,6 +26,7 @@ class FeatureId : protected simfil::MandatoryDerivedModelNodeBase<TileFeatureLay
     friend class TileFeatureLayer;
     friend class Feature;
     friend class bitsery::Access;
+    friend ModelNode::Ptr;
 
 public:
     /** Convert the FeatureId to a string like `<type-id>.<part-value-0>...<part-value-n>` */
diff --git a/libs/model/include/mapget/model/featurelayer.h b/libs/model/include/mapget/model/featurelayer.h
index 1da32bf3..8b7c5a17 100644
--- a/libs/model/include/mapget/model/featurelayer.h
+++ b/libs/model/include/mapget/model/featurelayer.h
@@ -159,6 +159,12 @@ class TileFeatureLayer : public TileLayer, public simfil::ModelPool
     /** (De-)Serialization */
    void write(std::ostream& outputStream) override;
 
+   /** Convert to GeoJSON geometry collection. */
+   nlohmann::json toGeoJson() const;
+
+   /** Shared pointer type */
+   using Ptr = std::shared_ptr<TileFeatureLayer>;
+
 protected:
     /**
      * The FeatureTileColumnId enum provides identifiers for different
diff --git a/libs/model/include/mapget/model/layer.h b/libs/model/include/mapget/model/layer.h
index 5048f385..5081abb2 100644
--- a/libs/model/include/mapget/model/layer.h
+++ b/libs/model/include/mapget/model/layer.h
@@ -4,6 +4,7 @@
 #include "tileid.h"
 
 #include "nlohmann/json.hpp"
+#include "simfil/model/nodes.h"
 
 #include <string>
 #include <chrono>
@@ -108,7 +109,7 @@ class TileLayer
      * and other arbitrary meta-information.
      */
     [[nodiscard]] nlohmann::json info() const;
-    void setInfo(const nlohmann::json& info);
+    void setInfo(std::string const& k, simfil::ScalarValueType const& v);
 
 protected:
     Version mapVersion_{0, 0, 0};
diff --git a/libs/model/include/mapget/model/stream.h b/libs/model/include/mapget/model/stream.h
index bbc75518..db93d262 100644
--- a/libs/model/include/mapget/model/stream.h
+++ b/libs/model/include/mapget/model/stream.h
@@ -47,7 +47,7 @@ class TileLayerStream
          */
         Reader(
             LayerInfoResolveFun layerInfoProvider,
-            std::function<void(std::shared_ptr<TileFeatureLayer>)> onParsedLayer,
+            std::function<void(TileFeatureLayer::Ptr)> onParsedLayer,
             std::shared_ptr<CachedFieldsProvider> fieldCacheProvider = nullptr);
 
         /**
@@ -71,7 +71,7 @@ class TileLayerStream
         std::stringstream buffer_;
         LayerInfoResolveFun layerInfoProvider_;
         std::shared_ptr<CachedFieldsProvider> fieldCacheProvider_;
-        std::function<void(std::shared_ptr<TileFeatureLayer>)> onParsedLayer_;
+        std::function<void(TileFeatureLayer::Ptr)> onParsedLayer_;
     };
 
     /**
@@ -93,7 +93,7 @@ class TileLayerStream
             FieldOffsetMap& fieldsOffsets);
 
         /** Serialize a tile feature layer and the required part of a Fields cache. */
-        void write(std::shared_ptr<TileFeatureLayer> const& tileFeatureLayer);
+        void write(TileFeatureLayer::Ptr const& tileFeatureLayer);
 
     private:
         void sendMessage(std::string const& bytes, MessageType msgType);
diff --git a/libs/model/src/feature.cpp b/libs/model/src/feature.cpp
index 28a238dd..ea0b0c57 100644
--- a/libs/model/src/feature.cpp
+++ b/libs/model/src/feature.cpp
@@ -204,6 +204,35 @@ nlohmann::json Feature::toJsonPrivate(const simfil::ModelNode& n)
     }
 }
 
+void Feature::addPoint(const Point& p) {
+    auto newGeom = geom()->newGeometry(GeomType::Points, 0);
+    newGeom->append(p);
+}
+
+void Feature::addPoints(const std::vector<Point>& points) {
+    auto newGeom = geom()->newGeometry(GeomType::Points, points.size()-1);
+    for (auto const& p : points)
+        newGeom->append(p);
+}
+
+void Feature::addLine(const std::vector<Point>& points) {
+    auto newGeom = geom()->newGeometry(GeomType::Line, points.size()-1);
+    for (auto const& p : points)
+        newGeom->append(p);
+}
+
+void Feature::addMesh(const std::vector<Point>& points) {
+    auto newGeom = geom()->newGeometry(GeomType::Mesh, points.size()-1);
+    for (auto const& p : points)
+        newGeom->append(p);
+}
+
+void Feature::addPoly(const std::vector<Point>& points) {
+    auto newGeom = geom()->newGeometry(GeomType::Polygon, points.size()-1);
+    for (auto const& p : points)
+        newGeom->append(p);
+}
+
 //////////////////////////////////////////
 
 Feature::FeaturePropertyView::FeaturePropertyView(
diff --git a/libs/model/src/featurelayer.cpp b/libs/model/src/featurelayer.cpp
index 0cfd4868..29cf747b 100644
--- a/libs/model/src/featurelayer.cpp
+++ b/libs/model/src/featurelayer.cpp
@@ -127,12 +127,19 @@ TileFeatureLayer::newFeatureId(
     const KeyValuePairs& featureIdParts)
 {
     // TODO: Validate ID parts
+    auto featureIdObject = newObject(featureIdParts.size());
     auto featureIdIndex = impl_->featureIds_.size();
     impl_->featureIds_.emplace_back(FeatureId::Data{
-        true,
+        false,
         fieldNames()->emplace(typeId),
-        newObject(featureIdParts.size())->addr()
+        featureIdObject->addr()
     });
+    for (auto const& [k, v] : featureIdParts) {
+        auto&& kk = k;
+        std::visit([&](auto&& x){
+            featureIdObject->addField(kk, x);
+        }, v);
+    }
     return FeatureId(impl_->featureIds_.back(), shared_from_this(), {FeatureIds, (uint32_t)featureIdIndex});
 }
 
@@ -308,4 +315,15 @@ void TileFeatureLayer::write(std::ostream& outputStream)
     ModelPool::write(outputStream);
 }
 
+nlohmann::json TileFeatureLayer::toGeoJson() const
+{
+    auto features = nlohmann::json::array();
+    for (auto f : *this)
+        features.push_back(f->toGeoJson());
+    return nlohmann::json::object({
+        {"type", "FeatureCollection"},
+        {"features", features},
+    });
+}
+
 }
diff --git a/libs/model/src/layer.cpp b/libs/model/src/layer.cpp
index 51a5edba..78e12182 100644
--- a/libs/model/src/layer.cpp
+++ b/libs/model/src/layer.cpp
@@ -140,8 +140,11 @@ void TileLayer::setProtocolVersion(Version version) {
     mapVersion_ = version;
 }
 
-void TileLayer::setInfo(const nlohmann::json& info) {
-    this->info_ = info;
+void TileLayer::setInfo(std::string const& k, simfil::ScalarValueType const& v) {
+    std::visit([this, &k](auto&& vv){
+       if constexpr (!std::is_same_v<std::decay_t<decltype(vv)>, std::monostate>)
+            info_[k] = vv;
+    }, v);
 }
 
 void TileLayer::write(std::ostream& outputStream)
diff --git a/libs/model/src/stream.cpp b/libs/model/src/stream.cpp
index 26b37565..520ab7a2 100644
--- a/libs/model/src/stream.cpp
+++ b/libs/model/src/stream.cpp
@@ -9,7 +9,7 @@ namespace mapget
 
 TileLayerStream::Reader::Reader(
     LayerInfoResolveFun layerInfoProvider,
-    std::function<void(std::shared_ptr<TileFeatureLayer>)> onParsedLayer,
+    std::function<void(TileFeatureLayer::Ptr)> onParsedLayer,
     std::shared_ptr<CachedFieldsProvider> fieldCacheProvider)
     : layerInfoProvider_(std::move(layerInfoProvider)),
       fieldCacheProvider_(
@@ -86,7 +86,7 @@ TileLayerStream::Writer::Writer(
 {
 }
 
-void TileLayerStream::Writer::write(std::shared_ptr<TileFeatureLayer> const& tileFeatureLayer)
+void TileLayerStream::Writer::write(TileFeatureLayer::Ptr const& tileFeatureLayer)
 {
     auto fields = tileFeatureLayer->fieldNames();
     auto& highestFieldKnownToClient = fieldsOffsets_[tileFeatureLayer->nodeId()];
diff --git a/libs/pymapget/CMakeLists.txt b/libs/pymapget/CMakeLists.txt
new file mode 100644
index 00000000..b65cbde2
--- /dev/null
+++ b/libs/pymapget/CMakeLists.txt
@@ -0,0 +1,42 @@
+project(pymapget)
+
+pybind11_add_module(mapget
+  MODULE
+    py-mapget.cpp
+    binding/py-model.h
+    binding/py-tileid.h
+    binding/py-layer.h
+    binding/py-datasource.h)
+
+if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
+  target_compile_options(mapget PUBLIC /permissive-)
+endif ()
+
+target_link_libraries(mapget
+  PRIVATE
+    mapget-model
+    mapget-datasource)
+
+target_include_directories(mapget
+  PRIVATE
+    binding)
+
+target_compile_features(mapget
+  INTERFACE
+    cxx_std_17)
+
+if (MSVC)
+  # Required because cpp-httplib speaks https via OpenSSL
+  set(DEPLOY_FILES
+    "${OPENSSL_INCLUDE_DIR}/../libcrypto-1_1-x64.dll"
+    "${OPENSSL_INCLUDE_DIR}/../libssl-1_1-x64.dll")
+endif ()
+
+add_wheel(mapget
+  AUTHOR "Klebert Engineering"
+  URL "https://github.com/klebert-engineering/mapget"
+  VERSION "${MAPGET_VERSION}"
+  DESCRIPTION "Python bindings for the mapget library."
+  TARGET_DEPENDENCIES mapget-model mapget-datasource
+  LICENSE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../LICENSE"
+  DEPLOY_FILES ${DEPLOY_FILES})
diff --git a/libs/pymapget/binding/py-datasource.h b/libs/pymapget/binding/py-datasource.h
new file mode 100644
index 00000000..63a8d598
--- /dev/null
+++ b/libs/pymapget/binding/py-datasource.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include "mapget/datasource/server.h"
+
+#include <pybind11/functional.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+namespace py = pybind11;
+using namespace py::literals;
+using namespace mapget;
+using namespace simfil;
+
+void bindDataSource(py::module_& m)
+{
+    py::class_<DataSourceServer, std::shared_ptr<DataSourceServer>>(m, "DataSourceServer")
+        .def(
+            py::init(
+                [](py::dict const& dict)
+                {
+                    // import json.dumps
+                    py::module json_module = py::module::import("json");
+                    py::function json_dumps = json_module.attr("dumps");
+
+                    // convert py::dict to JSON string
+                    auto json_str = json_dumps(dict).cast<std::string>();
+
+                    // parse JSON string into nlohmann::json
+                    nlohmann::json j = nlohmann::json::parse(json_str);
+
+                    // construct DataSource
+                    return std::make_unique<DataSourceServer>(DataSourceInfo::fromJson(j));
+                }),
+            R"pbdoc(
+                Construct a DataSource with a DataSourceInfo metadata instance.
+            )pbdoc",
+            py::arg("info_dict"))
+        .def(
+            "on_tile_request",
+            &DataSourceServer::onTileRequest,
+            py::arg("callback"),
+            py::call_guard<py::gil_scoped_acquire>(),
+            R"pbdoc(
+            Set the Callback which will be invoked when a `/tile`-request is received.
+            The callback argument is a fresh TileFeatureLayer, which the callback must
+            fill according to the set TileFeatureLayer's layer info and tile id. If an
+            error occurs while filling the tile, the callback can use
+            TileFeatureLayer::setError(...) to signal the error downstream.
+        )pbdoc")
+        .def(
+            "go",
+            &DataSourceServer::go,
+            py::arg("interfaceAddr") = "0.0.0.0",
+            py::arg("port") = 0,
+            py::arg("waitMs") = 100,
+            R"pbdoc(
+            Launch the DataSource server in its own thread. Use the stop-function to
+            stop the thread. The server will also be stopped automatically, if the
+            DataSource object is destroyed. An exception will be thrown if this
+            instance is already running, or if the server fails to launch within waitMs.
+        )pbdoc")
+        .def(
+            "is_running",
+            &DataSourceServer::isRunning,
+            R"pbdoc(
+            Returns true if this instance is currently running (go() was called and not stopped).
+        )pbdoc")
+        .def(
+            "stop",
+            &DataSourceServer::stop,
+            R"pbdoc(
+            Stop this instance. Will be a no-op if this instance is not running.
+        )pbdoc")
+        .def(
+            "wait_for_signal",
+            &DataSourceServer::waitForSignal,
+            py::call_guard<py::gil_scoped_release>(),
+            R"pbdoc(
+            Blocks until SIGINT or SIGTERM is received, then shuts down the server.
+            Note: You can never run this function in parallel for multiple sources
+            within the same process!
+        )pbdoc")
+        .def(
+            "port",
+            &DataSourceServer::port,
+            R"pbdoc(
+            Get the port currently used by the instance, or 0 if go() has never been called.
+        )pbdoc")
+        .def(
+            "info",
+            &DataSourceServer::info,
+            R"pbdoc(
+            Get the DataSourceInfo metadata which this instance was constructed with.
+        )pbdoc");
+}
\ No newline at end of file
diff --git a/libs/pymapget/binding/py-layer.h b/libs/pymapget/binding/py-layer.h
new file mode 100644
index 00000000..f6454e76
--- /dev/null
+++ b/libs/pymapget/binding/py-layer.h
@@ -0,0 +1,162 @@
+#pragma once
+
+#include "mapget/model/featurelayer.h"
+
+#include <pybind11/functional.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+#include "py-model.h"
+
+namespace py = pybind11;
+using namespace py::literals;
+using namespace mapget;
+using namespace simfil;
+
+void bindTileLayer(py::module_& m)
+{
+    py::class_<TileFeatureLayer, TileFeatureLayer::Ptr>(
+        m,
+        "TileFeatureLayer")
+        .def(
+            "tile_id",
+            [](TileFeatureLayer const& self){return self.tileId();},
+            R"pbdoc(
+            Get the layer's tileId. This controls the rough geographic extent
+            of the contained tile data.
+            )pbdoc")
+        .def(
+            "map_id",
+            [](TileFeatureLayer const& self){return self.mapId();},
+            R"pbdoc(
+            Get the identifier of the map which this tile layer belongs to.
+            )pbdoc")
+        .def(
+            "layer_id",
+            [](TileFeatureLayer const& self) { return self.layerInfo()->layerId_; },
+            R"pbdoc(
+            Get the layer name for this TileLayer.
+            )pbdoc")
+        .def(
+            "error",
+            [](TileFeatureLayer const& self) { return self.error(); },
+            R"pbdoc(
+            Get the error occurred while the tile was filled.
+            )pbdoc")
+        .def(
+            "set_error",
+            [](TileFeatureLayer& self, std::string const& e) { self.setError(e); },
+            py::arg("err"),
+            R"pbdoc(
+            Set the error occurred while the tile was filled.
+            )pbdoc")
+        .def(
+            "timestamp",
+            [](TileFeatureLayer const& self) {return self.timestamp(); },
+            R"pbdoc(
+            Get when this layer was created.
+            )pbdoc")
+        .def(
+            "ttl",
+            [](TileFeatureLayer const& self) {return self.ttl() ? self.ttl()->count() : -1; },
+            R"pbdoc(
+            Get how long this layer should live, or -1 if unset.
+            )pbdoc")
+        .def(
+            "set_ttl",
+            [](TileFeatureLayer& self, int64_t ms) {
+                if (ms >= 0)
+                    self.setTtl(std::chrono::milliseconds(ms));
+                else
+                    self.setTtl(std::nullopt);
+            },
+            py::arg("time_to_live_in_ms"),
+            R"pbdoc(
+            Set how long this layer should live, in ms, or -1 for unset.
+            )pbdoc")
+        .def(
+            "set_info",
+            [](TileFeatureLayer& self, std::string const& k, simfil::ScalarValueType const& v) {
+                self.setInfo(k, v);
+            },
+            py::arg("key"),
+            py::arg("value"),
+            R"pbdoc(
+            Set a JSON field to store sizes, construction times,
+            and other arbitrary meta-information. The value may be
+            bool, int, double or string.
+        )pbdoc")
+        .def(
+            "set_prefix",
+            [](TileFeatureLayer& self, KeyValuePairs const& v) {self.setPrefix(v); },
+            py::arg("prefix"),
+            R"pbdoc(
+            Set common id prefix for all features in this layer.
+        )pbdoc")
+        .def(
+            "new_feature",
+            [](TileFeatureLayer& self, std::string const& typeId, KeyValuePairs const& idParts)
+            { return BoundFeature(self.newFeature(typeId, idParts)); },
+            py::arg("type_id"),
+            py::arg("feature_id_parts"),
+            R"pbdoc(
+            Creates a new feature and insert it into this tile layer. The unique identifying
+            information, prepended with the featureIdPrefix, must conform to an existing
+            UniqueIdComposition for the feature typeId within the associated layer.
+        )pbdoc")
+        .def(
+            "new_feature_id",
+            [](TileFeatureLayer& self, std::string const& typeId, KeyValuePairs const& idParts)
+            { return BoundFeatureId(self.newFeatureId(typeId, idParts)); },
+            py::arg("type_id"),
+            py::arg("feature_id_parts"),
+            R"pbdoc(
+            Create a new feature id. Use this function to create a reference to another
+            feature. The created feature id will not use the common feature id prefix
+            from this tile feature layer.
+        )pbdoc")
+        .def(
+            "new_attribute",
+            [](TileFeatureLayer& self, std::string const& name)
+            { return BoundAttribute(self.newAttribute(name)); },
+            py::arg("name"),
+            R"pbdoc(
+            Create a new named attribute, which may be inserted into an attribute layer.
+        )pbdoc")
+        .def(
+            "new_attribute_layer",
+            [](TileFeatureLayer& self)
+            { return BoundAttributeLayer(self.newAttributeLayer()); },
+            R"pbdoc(
+            Create a new attribute layer, which may be inserted into a feature.
+        )pbdoc")
+        .def(
+            "new_object",
+            [](TileFeatureLayer& self)
+            { return BoundObject(self.newObject()); },
+            R"pbdoc(
+            Adopt members from the given vector and obtain a new object model index which has these members.
+        )pbdoc")
+        .def(
+            "new_array",
+            [](TileFeatureLayer& self)
+            { return BoundArray(self.newArray()); },
+            R"pbdoc(
+            Adopt members from the given vector and obtain a new array model index which has these members.
+        )pbdoc")
+        .def(
+            "new_geometry_collection",
+            [](TileFeatureLayer& self)
+            { return BoundGeometryCollection(self.newGeometryCollection()); },
+            R"pbdoc(
+            Create a new geometry collection.
+        )pbdoc")
+        .def(
+            "new_geometry",
+            [](TileFeatureLayer& self, Geometry::GeomType const& geomType)
+            { return BoundGeometry(self.newGeometry(geomType)); },
+            py::arg("geom_type"),
+            R"pbdoc(
+            Create a new geometry of the given type.
+        )pbdoc");
+}
diff --git a/libs/pymapget/binding/py-model.h b/libs/pymapget/binding/py-model.h
new file mode 100644
index 00000000..20e67b5b
--- /dev/null
+++ b/libs/pymapget/binding/py-model.h
@@ -0,0 +1,464 @@
+#pragma once
+
+#include "mapget/model/feature.h"
+#include "mapget/model/featurelayer.h"
+#include "simfil/value.h"
+
+#include <pybind11/functional.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+namespace py = pybind11;
+using namespace py::literals;
+using namespace simfil;
+
+namespace mapget
+{
+
+struct BoundModelNode
+{
+    virtual ~BoundModelNode() = default;
+
+    virtual ModelNode::Ptr node() = 0;
+
+    TileFeatureLayer::Ptr model()
+    {
+        struct GetTileFeatureLayer : public simfil::ModelNode {
+            explicit GetTileFeatureLayer(simfil::ModelNode const& n) : simfil::ModelNode(n) {}
+            TileFeatureLayer::Ptr operator()() {
+                return std::reinterpret_pointer_cast<TileFeatureLayer>(
+                    std::const_pointer_cast<simfil::Model>(model_));
+            }
+        };
+        return GetTileFeatureLayer(*node())();
+    }
+};
+
+struct BoundModelNodeBase : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::class_<BoundModelNode>(m, "ModelNode")
+            .def(
+                "value",
+                [](const shared_model_ptr<ModelNode>& node) { return node->value(); },
+                R"pbdoc(
+            Get the node's scalar value if it has one.
+        )pbdoc");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    shared_model_ptr<ModelNode> modelNodePtr_;
+};
+
+namespace
+{
+
+using ModelVariant =
+    std::variant<bool, int16_t, int64_t, double, std::string_view, ModelNode::Ptr >;
+
+ModelVariant py_value_to_model(py::object const& py_value, TileFeatureLayer& model)
+{
+    if (py::isinstance<py::bool_>(py_value)) {
+        return py_value.cast<bool>();
+    }
+    else if (py::isinstance<py::int_>(py_value)) {
+        auto value = py_value.cast<int64_t>();
+        if (value >= INT16_MIN && value <= INT16_MAX) {
+            return static_cast<int16_t>(value);
+        }
+        else {
+            return value;
+        }
+    }
+    else if (py::isinstance<py::float_>(py_value)) {
+        return py_value.cast<double>();
+    }
+    else if (py::isinstance<py::str>(py_value)) {
+        return py_value.cast<std::string_view>();
+    }
+    else if (py::isinstance<BoundModelNode>(py_value)) {
+        return py_value.cast<BoundModelNode&>().node();
+    }
+    else if (py::isinstance<py::list>(py_value)) {
+        // Recursively convert Python list to array.
+        auto list = py_value.cast<py::list>();
+        auto arr = model.newArray(list.size());
+
+        for (auto const& item : list) {
+            auto value = py_value_to_model(py::reinterpret_borrow<py::object>(item), model);
+            std::visit([&arr](auto&& vv){
+                arr->append(vv);
+            }, value);
+        }
+
+        return ModelNode::Ptr(arr);
+    }
+    else if (py::isinstance<py::dict>(py_value)) {
+        // Recursively convert Python dict to object.
+        auto dict = py_value.cast<py::dict>();
+        auto obj = model.newObject(dict.size());
+
+        for (auto const& [anyKey, anyValue] : dict) {
+            std::string key = py::str(anyKey);
+            auto vv = py_value_to_model(py::reinterpret_borrow<py::object>(anyValue), model);
+            std::visit(
+                [&obj, &key](auto&& value)
+                {
+                    if constexpr (std::is_same<std::decay_t<decltype(value)>, bool>::value)
+                        obj->addBool(key, value);
+                    else
+                        obj->addField(key, value);
+                },
+                vv);
+        }
+
+        return ModelNode::Ptr(obj);
+    }
+    else {
+        throw std::runtime_error("Unsupported Python type");
+    }
+}
+}  // namespace
+
+template <typename NodeType = Object>
+struct BoundObject : public BoundModelNode
+{
+    template <class ObjClass, class ParentClass>
+    static void bindObjectMethods(py::class_<ObjClass, ParentClass>& c)
+    {
+        c.def(
+            "add_field",
+            [](ObjClass& self, std::string_view const& name, py::object const& py_value)
+            {
+                auto vv = py_value_to_model(py_value, *self.model());
+                std::visit(
+                    [&self, &name](auto&& value)
+                    {
+                        if constexpr (std::is_same<std::decay_t<decltype(value)>, bool>::value) {
+                            self.modelNodePtr_->addBool(name, value);
+                        }
+                        else {
+                            self.modelNodePtr_->addField(name, value);
+                        }
+                    },
+                    vv);
+            },
+            py::arg("name"),
+            py::arg("value"),
+            "Add a field to the object.");
+    }
+
+    static void bind(py::module_& m)
+    {
+        auto boundClass = py::class_<BoundObject, BoundModelNode>(m, "Object");
+        bindObjectMethods(boundClass);
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundObject(model_ptr<NodeType> const& ptr) : modelNodePtr_(ptr) {}
+
+    shared_model_ptr<NodeType> modelNodePtr_;
+};
+
+struct BoundArray : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::class_<BoundArray, BoundModelNode>(m, "Array")
+            .def(
+                "append",
+                [](BoundArray& self, py::object const& py_value) {
+                    auto vv = py_value_to_model(
+                        py_value,
+                        *self.model());
+                    std::visit([&self](auto&& value) { self.modelNodePtr_->append(value); }, vv);
+                },
+                py::arg("value"),
+                "Append a value to the array.");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundArray(model_ptr<Array> const& ptr) : modelNodePtr_(ptr) {}
+
+    shared_model_ptr<Array> modelNodePtr_;
+};
+
+struct BoundGeometry : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::enum_<Geometry::GeomType>(m, "GeomType")
+            .value("LINE", Geometry::GeomType::Line)
+            .value("MESH", Geometry::GeomType::Mesh)
+            .value("POINTS", Geometry::GeomType::Points)
+            .value("POLYGON", Geometry::GeomType::Polygon);
+
+        py::class_<BoundGeometry, BoundModelNode>(m, "Geometry")
+            .def(
+                "append",
+                [](BoundGeometry& node, double const& lon, double const& lat, double const& alt) {
+                    node.modelNodePtr_->append({lon, lat, alt});
+                },
+                py::arg("lon"),
+                py::arg("lat"),
+                py::arg("elevation") = .0,
+                R"pbdoc(
+                Append a point to the geometry.
+            )pbdoc")
+            .def(
+                "append",
+                [](BoundGeometry& node, Point const& p) {
+                    node.modelNodePtr_->append(p);
+                },
+                py::arg("point"),
+                R"pbdoc(
+                Append a point to the geometry.
+            )pbdoc");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundGeometry(model_ptr<Geometry> const& ptr) : modelNodePtr_(ptr) {}
+
+    shared_model_ptr<simfil::Geometry> modelNodePtr_;
+};
+
+struct BoundGeometryCollection : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::class_<BoundGeometryCollection, BoundModelNode>(m, "GeometryCollection")
+            .def(
+                "new_geometry",
+                [](BoundGeometryCollection& self, Geometry::GeomType const& geomType)
+                { return BoundGeometry(self.modelNodePtr_->newGeometry(geomType)); },
+                py::arg("geom_type"),
+                "Create and insert a new geometry into the collection.");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundGeometryCollection(model_ptr<GeometryCollection> const& ptr) : modelNodePtr_(ptr)
+    {
+    }
+
+    shared_model_ptr<simfil::GeometryCollection> modelNodePtr_;
+};
+
+struct BoundAttribute : public BoundObject<Attribute>
+{
+    static void bind(py::module_& m)
+    {
+        py::enum_<Attribute::Direction>(m, "Direction")
+            .value("EMPTY", Attribute::Direction::Empty)
+            .value("POSITIVE", Attribute::Direction::Positive)
+            .value("NEGATIVE", Attribute::Direction::Negative)
+            .value("BOTH", Attribute::Direction::Both)
+            .value("NONE", Attribute::Direction::None);
+
+        auto boundClass =
+            py::class_<BoundAttribute, BoundModelNode>(m, "Attribute")
+                .def(
+                    "direction",
+                    [](BoundAttribute& self) { return self.modelNodePtr_->direction(); },
+                    "Get the direction of the attribute.")
+                .def(
+                    "set_direction",
+                    [](BoundAttribute& self, Attribute::Direction const& v)
+                    { self.modelNodePtr_->setDirection(v); },
+                    py::arg("v"),
+                    "Set the direction of the attribute.")
+                .def(
+                    "validity",
+                    [](BoundAttribute& self) { return self.modelNodePtr_->validity(); },
+                    "Get the validity of the attribute.")
+                .def(
+                    "name",
+                    [](BoundAttribute& self) { return self.modelNodePtr_->name(); },
+                    "Get the name of the attribute.");
+
+        bindObjectMethods(boundClass);
+    }
+
+    explicit BoundAttribute(model_ptr<Attribute> const& ptr) : BoundObject<Attribute>(ptr) {}
+};
+
+struct BoundAttributeLayer : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::class_<BoundAttributeLayer, BoundModelNode>(m, "AttributeLayer")
+            .def(
+                "new_attribute",
+                [](BoundAttributeLayer& self, std::string_view const& name)
+                { return BoundAttribute{self.modelNodePtr_->newAttribute(name)}; },
+                py::arg("name"),
+                "Create and insert a new attribute into the layer.")
+            .def(
+                "add_attribute",
+                [](BoundAttributeLayer& self, BoundAttribute const& a)
+                { self.modelNodePtr_->addAttribute(a.modelNodePtr_); },
+                py::arg("a"),
+                "Add an existing attribute to the layer.");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundAttributeLayer(model_ptr<AttributeLayer> const& ptr) : modelNodePtr_(ptr) {}
+
+    shared_model_ptr<AttributeLayer> modelNodePtr_;
+};
+
+struct BoundAttributeLayerList : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::class_<BoundAttributeLayerList, BoundModelNode>(m, "AttributeLayerList")
+            .def(
+                "new_layer",
+                [](BoundAttributeLayerList& self, std::string_view const& name)
+                { return BoundAttributeLayer(self.modelNodePtr_->newLayer(name)); },
+                py::arg("name"),
+                "Create and insert a new layer into the collection.")
+            .def(
+                "add_layer",
+                [](BoundAttributeLayerList& self,
+                   std::string_view const& name,
+                   BoundAttributeLayer const& l)
+                { self.modelNodePtr_->addLayer(name, l.modelNodePtr_); },
+                py::arg("name"),
+                py::arg("layer"),
+                "Add an existing layer to the collection.");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundAttributeLayerList(model_ptr<AttributeLayerList> const& ptr) : modelNodePtr_(ptr)
+    {
+    }
+
+    shared_model_ptr<AttributeLayerList> modelNodePtr_;
+};
+
+struct BoundFeature : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::class_<BoundFeature, BoundModelNode>(m, "Feature")
+            .def(
+                "type_id",
+                [](BoundFeature& self) { return self.modelNodePtr_->typeId(); },
+                "Get the type ID of the feature.")
+            .def(
+                "evaluate",
+                [](BoundFeature& self, std::string_view const& expression)
+                { return self.modelNodePtr_->evaluate(expression).getScalar(); },
+                py::arg("expression"),
+                "Evaluate a filter expression on this feature.")
+            .def(
+                "to_geo_json",
+                [](BoundFeature& self) { return self.modelNodePtr_->toGeoJson(); },
+                "Convert the Feature to GeoJSON.")
+            .def(
+                "geom",
+                [](BoundFeature& self)
+                { return BoundGeometryCollection(self.modelNodePtr_->geom()); },
+                "Access this feature's geometry collection.")
+            .def(
+                "attributes",
+                [](BoundFeature& self) { return BoundObject(self.modelNodePtr_->attributes()); },
+                "Access this feature's arbitrary attributes.")
+            .def(
+                "attribute_layers",
+                [](BoundFeature& self)
+                { return BoundAttributeLayerList(self.modelNodePtr_->attributeLayers()); },
+                "Access this feature's attribute layer collection.")
+            .def(
+                "children",
+                [](BoundFeature& self) { return BoundArray(self.modelNodePtr_->children()); },
+                "Access this feature's child feature id list.")
+                .def(
+                    "add_point",
+                    [](BoundFeature& self, Point const& p) {
+                        self.modelNodePtr_->addPoint(p);
+                    },
+                    py::arg("p"),
+                    "Add a point to the feature.")
+                .def(
+                    "add_points",
+                    [](BoundFeature& self, std::vector<Point> const& points) {
+                        self.modelNodePtr_->addPoints(points);
+                    },
+                    py::arg("points"),
+                    "Add multiple points to the feature.")
+                .def(
+                    "add_line",
+                    [](BoundFeature& self, std::vector<Point> const& points) {
+                        self.modelNodePtr_->addLine(points);
+                    },
+                    py::arg("points"),
+                    "Add a line to the feature.")
+                .def(
+                    "add_mesh",
+                    [](BoundFeature& self, std::vector<Point> const& points) {
+                        self.modelNodePtr_->addMesh(points);
+                    },
+                    py::arg("points"),
+                    "Add a mesh to the feature, len(points) must be multiple of three.")
+                .def(
+                    "add_poly",
+                    [](BoundFeature& self, std::vector<Point> const& points) {
+                        self.modelNodePtr_->addPoly(points);
+                    },
+                    py::arg("points"),
+                    "Add a polygon to the feature.");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundFeature(model_ptr<Feature> const& ptr) : modelNodePtr_(ptr) {}
+
+    shared_model_ptr<Feature> modelNodePtr_;
+};
+
+struct BoundFeatureId : public BoundModelNode
+{
+    static void bind(py::module_& m)
+    {
+        py::class_<BoundFeatureId, BoundModelNode>(m, "FeatureId")
+            .def(
+                "to_string",
+                [](BoundFeatureId& self) { return self.modelNodePtr_->toString(); },
+                "Convert the FeatureId to a string.")
+            .def(
+                "type_id",
+                [](BoundFeatureId& self) { return self.modelNodePtr_->typeId(); },
+                "Get the feature ID's type ID.");
+    }
+
+    ModelNode::Ptr node() override { return modelNodePtr_; }
+
+    explicit BoundFeatureId(model_ptr<FeatureId> const& ptr) : modelNodePtr_(ptr) {}
+
+    shared_model_ptr<FeatureId> modelNodePtr_;
+};
+
+}  // namespace mapget
+
+void bindModel(py::module& m)
+{
+    mapget::BoundModelNodeBase::bind(m);
+    mapget::BoundObject<>::bind(m);
+    mapget::BoundArray::bind(m);
+    mapget::BoundGeometry::bind(m);
+    mapget::BoundGeometryCollection::bind(m);
+    mapget::BoundAttribute::bind(m);
+    mapget::BoundAttributeLayer::bind(m);
+    mapget::BoundAttributeLayerList::bind(m);
+    mapget::BoundFeature::bind(m);
+    mapget::BoundFeatureId::bind(m);
+}
diff --git a/libs/pymapget/binding/py-tileid.h b/libs/pymapget/binding/py-tileid.h
new file mode 100644
index 00000000..c383aaec
--- /dev/null
+++ b/libs/pymapget/binding/py-tileid.h
@@ -0,0 +1,106 @@
+#pragma once
+
+#include "mapget/model/tileid.h"
+
+#include <pybind11/functional.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+using namespace mapget;
+
+void bindTileId(py::module_& m)
+{
+    py::class_<Point>(m, "Point", R"pbdoc(
+            The Point class represents a point in 3D space with x, y, and z components.
+        )pbdoc")
+        .def(py::init<>(), R"pbdoc(
+            Default constructor initializing a Point with x, y, and z set to 0.
+            )pbdoc")
+        .def(
+            py::init<double, double, double>(),
+            R"pbdoc(
+            Constructor initializing a Point with given x, y, and z.
+            )pbdoc",
+            py::arg("x") = 0,
+            py::arg("y") = 0,
+            py::arg("z") = 0)
+        .def_readwrite("x", &Point::x, "The x-component of the point.")
+        .def_readwrite("y", &Point::y, "The y-component of the point.")
+        .def_readwrite("z", &Point::z, "The z-component of the point.")
+        .def(
+            "__eq__",
+            &Point::operator==,
+            R"pbdoc(
+            Operator overload for equality comparison with another Point.
+            )pbdoc",
+            py::arg("other"))
+        .def("__str__", &Point::toString, R"pbdoc(
+            Convert the Point to a string representation.
+            )pbdoc");
+
+    py::class_<TileId>(m, "TileId", R"pbdoc(
+            The TileId struct represents a tile identifier for a specific map tile.
+            It includes an x (column), y (row), and z (zoom level) components.
+            )pbdoc")
+        .def(
+            py::init<uint16_t, uint16_t, uint16_t>(),
+            R"pbdoc(
+            Constructor to initialize TileId with x, y, and z values.
+            )pbdoc",
+            py::arg("x"),
+            py::arg("y"),
+            py::arg("z"))
+        .def(
+            py::init<uint64_t>(),
+            R"pbdoc(
+            Constructor to initialize TileId with a value in 0x00xxyyzz format.
+            )pbdoc",
+            py::arg("value"))
+        .def_static(
+            "from_wgs84",
+            &TileId::fromWgs84,
+            R"pbdoc(
+            Create a TileId from WGS84 longitude, latitude, and zoom level.
+            )pbdoc",
+            py::arg("longitude"),
+            py::arg("latitude"),
+            py::arg("zoom_level"))
+        .def("center", &TileId::center, R"pbdoc(
+            Get the center of the tile in WGS84 coordinates.
+            )pbdoc")
+        .def("sw", &TileId::sw, R"pbdoc(
+            Get the south-west (minimum) corner of the tile in WGS84 coordinates.
+            )pbdoc")
+        .def("ne", &TileId::ne, R"pbdoc(
+            Get the north-east (maximum) corner of the tile in WGS84 coordinates.
+            )pbdoc")
+        .def("size", &TileId::size, R"pbdoc(
+            Get the size of the tile in WGS84 coordinates.
+            )pbdoc")
+        .def("x", &TileId::x, R"pbdoc(
+            Get the x (column) component of the TileId.
+            )pbdoc")
+        .def("y", &TileId::y, R"pbdoc(
+            Get the y (row) component of the TileId.
+            )pbdoc")
+        .def("z", &TileId::z, R"pbdoc(
+            Get the z (zoom level) component of the TileId.
+            )pbdoc")
+        .def(
+            "__eq__",
+            &TileId::operator==,
+            R"pbdoc(
+            Operator overload for equality comparison with another TileId.
+            )pbdoc",
+            py::arg("other"))
+        .def(
+            "__lt__",
+            &TileId::operator<,
+            R"pbdoc(
+            Operator overload for less than comparison with another TileId.
+            )pbdoc",
+            py::arg("other"))
+        .def_readwrite("value", &TileId::value_, R"pbdoc(
+            The value representing the TileId, in 0x00xxyyzz format.
+            )pbdoc");
+}
diff --git a/libs/pymapget/py-mapget.cpp b/libs/pymapget/py-mapget.cpp
new file mode 100644
index 00000000..3dba2e26
--- /dev/null
+++ b/libs/pymapget/py-mapget.cpp
@@ -0,0 +1,24 @@
+#include <pybind11/pybind11.h>
+#include <pybind11/stl_bind.h>
+#include <pybind11/stl.h>
+#include <pybind11/functional.h>
+#include "stx/format.h"
+
+namespace py = pybind11;
+using namespace py::literals;
+using namespace std::string_literals;
+
+#include "binding/py-tileid.h"
+#include "binding/py-model.h"
+#include "binding/py-layer.h"
+#include "binding/py-datasource.h"
+
+PYBIND11_MODULE(mapget, m)
+{
+    m.doc() = "";
+
+    bindTileId(m);
+    bindModel(m);
+    bindTileLayer(m);
+    bindDataSource(m);
+}


Test patch:
diff --git a/test/datasource.cpp b/test/datasource.cpp
index 12a73829..57a55aca 100644
--- a/test/datasource.cpp
+++ b/test/datasource.cpp
@@ -44,8 +44,10 @@ TEST_CASE("DataSource", "[DataSource]")
     // Initialize a DataSource
     DataSourceServer ds(info);
 
-    ds.onTileRequest([](TileFeatureLayer& tile) {
-        auto f = tile.newFeature("Way", {{"areaId", "Area42"}, {"wayId", 0}});
+    ds.onTileRequest([](TileFeatureLayer::Ptr tile) {
+        constexpr auto ExpectingThisTileId = 1;
+        REQUIRE(tile->tileId() == ExpectingThisTileId);
+        auto f = tile->newFeature("Way", {{"areaId", "Area42"}, {"wayId", 0}});
         auto g = f->geom()->newGeometry(GeomType::Line);
         g->append({42., 11});
         g->append({42., 12});
diff --git a/test/model.cpp b/test/model.cpp
index 0f444dcd..79749013 100644
--- a/test/model.cpp
+++ b/test/model.cpp
@@ -5,6 +5,7 @@
 #include "nlohmann/json.hpp"
 
 #include <sstream>
+#include <iostream>
 
 using namespace mapget;
 
@@ -54,6 +55,13 @@ TEST_CASE("FeatureLayer", "[test.featurelayer]")
     line->append({41., 10.});
     line->append({43., 11.});
 
+    // Use high-level geometry API
+    feature1->addPoint({41.5, 10.5, 0});
+    feature1->addPoints({{41.5, 10.5, 0}, {41.6, 10.7}});
+    feature1->addLine({{41.5, 10.5, 0}, {41.6, 10.7}});
+    feature1->addMesh({{41.5, 10.5, 0}, {41.6, 10.7}, {41.5, 10.3}});
+    feature1->addPoly({{41.5, 10.5, 0}, {41.6, 10.7}, {41.5, 10.3}, {41.8, 10.9}});
+
     // Add a fixed attribute
     feature1->attributes()->addField("main_ingredient", "Pepper");
 
@@ -65,9 +73,10 @@ TEST_CASE("FeatureLayer", "[test.featurelayer]")
 
     SECTION("toGeoJSON")
     {
-        constexpr auto expected = R"({"areaId":"TheBestArea","geometry":{"geometries":[{"coordinates":[[41.0,10.0,0.0],[43.0,11.0,0.0]],"type":"LineString"}],"type":"GeometryCollection"},"id":"Way.TheBestArea.42","properties":{"layer":{"cheese":{"mozzarella":{"direction":1,"smell":"neutral"}}},"main_ingredient":"Pepper"},"type":"Feature","typeId":"Way","wayId":42})";
+        constexpr auto expected = R"({"areaId":"TheBestArea","geometry":{"geometries":[{"coordinates":[[41.0,10.0,0.0],[43.0,11.0,0.0]],"type":"LineString"},{"coordinates":[[41.5,10.5,0.0]],"type":"MultiPoint"},{"coordinates":[[41.5,10.5,0.0],[41.600000001490116,10.700000002980232,0.0]],"type":"MultiPoint"},{"coordinates":[[41.5,10.5,0.0],[41.600000001490116,10.700000002980232,0.0]],"type":"LineString"},{"coordinates":[[41.5,10.5,0.0],[41.600000001490116,10.700000002980232,0.0],[41.5,10.299999997019768,0.0]],"type":"MultiPolygon"},{"coordinates":[[41.5,10.5,0.0],[41.600000001490116,10.700000002980232,0.0],[41.5,10.299999997019768,0.0],[41.80000001192093,10.900000005960464,0.0]],"type":"Polygon"}],"type":"GeometryCollection"},"id":"Way.TheBestArea.42","properties":{"layer":{"cheese":{"mozzarella":{"direction":1,"smell":"neutral"}}},"main_ingredient":"Pepper"},"type":"Feature","typeId":"Way","wayId":42})";
         std::stringstream featureGeoJson;
         featureGeoJson << feature1->toGeoJson();
+        std::cout << featureGeoJson.str() << std::endl;
         REQUIRE(featureGeoJson.str() == expected);
     }
 
@@ -82,7 +91,7 @@ TEST_CASE("FeatureLayer", "[test.featurelayer]")
         REQUIRE(feature1->evaluate("**.mozzarella.smell").toString() == "neutral");
         REQUIRE(feature1->evaluate("properties.main_ingredient").toString() == "Pepper");
         REQUIRE(
-            feature1->evaluate("geo() within bbox(40., 9., 45., 12.)")
+            feature1->evaluate("any(geo() within bbox(40., 9., 45., 12.))")
                 .toString() == "true");
     }
 
@@ -158,7 +167,7 @@ TEST_CASE("FeatureLayer", "[test.featurelayer]")
         // always waits until the full message is received before trying
         // to parse an object.
 
-        std::vector<std::shared_ptr<TileFeatureLayer>> readTiles;
+        std::vector<TileFeatureLayer::Ptr> readTiles;
         TileLayerStream::Reader reader{
             [&](auto&& mapId, auto&& layerId) { return layerInfo; },
             [&](auto&& layerPtr) { readTiles.push_back(layerPtr); },