From cf1261444f2e2e6bbcfd1fa6fecb1321eacd688f Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 28 Aug 2026 12:37:54 +0200 Subject: [PATCH 001/101] Initial commit --- Notes.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Notes.md diff --git a/Notes.md b/Notes.md new file mode 100644 index 0000000..bbd29b1 --- /dev/null +++ b/Notes.md @@ -0,0 +1,9 @@ +# Notes + +## GeoJSON + +* [New Features and Changes in NGSI-LD v1.3.1](https://docbox.etsi.org/ISG/CIM/Open/NGSI-LD_API_v1.3.1-New_Features_and_Changes.pdf) +* https://github.com/ScorpioBroker/ScorpioBroker/issues/128 + * https://github.com/ScorpioBroker/ScorpioBroker/issues/576 + +Is a `location` property the way to store location data on an entity? From 32dea1e3c603215fac695918b27dae78207e9dd4 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 28 Aug 2026 12:58:14 +0200 Subject: [PATCH 002/101] Added DDEV and Symfony --- .ddev/config.yaml | 295 ++++ .editorconfig | 17 + .env | 27 + .env.dev | 4 + .gitignore | 10 + README.md | 7 + bin/console | 21 + composer.json | 71 + composer.lock | 2608 ++++++++++++++++++++++++++++++++ config/bundles.php | 5 + config/packages/cache.yaml | 19 + config/packages/framework.yaml | 15 + config/packages/routing.yaml | 10 + config/preload.php | 5 + config/reference.php | 826 ++++++++++ config/routes.yaml | 11 + config/routes/framework.yaml | 4 + config/services.yaml | 23 + public/index.php | 9 + src/Controller/.gitignore | 0 src/Kernel.php | 19 + symfony.lock | 60 + 22 files changed, 4066 insertions(+) create mode 100644 .ddev/config.yaml create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .env.dev create mode 100644 .gitignore create mode 100755 bin/console create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/bundles.php create mode 100644 config/packages/cache.yaml create mode 100644 config/packages/framework.yaml create mode 100644 config/packages/routing.yaml create mode 100644 config/preload.php create mode 100644 config/reference.php create mode 100644 config/routes.yaml create mode 100644 config/routes/framework.yaml create mode 100644 config/services.yaml create mode 100644 public/index.php create mode 100644 src/Controller/.gitignore create mode 100644 src/Kernel.php create mode 100644 symfony.lock diff --git a/.ddev/config.yaml b/.ddev/config.yaml new file mode 100644 index 0000000..0594bc0 --- /dev/null +++ b/.ddev/config.yaml @@ -0,0 +1,295 @@ +name: enter +type: symfony +docroot: public +php_version: "8.5" +webserver_type: nginx-fpm +xdebug_enabled: false +additional_hostnames: [] +additional_fqdns: [] +database: + type: mariadb + version: "11.8" +use_dns_when_possible: true +composer_version: "2" +web_environment: [] +nodejs_version: "24" +corepack_enable: false + +# Key features of DDEV's config.yaml: + +# name: # Name of the project, automatically provides +# http://projectname.ddev.site and https://projectname.ddev.site +# If the name is omitted, the project will take the name of the enclosing directory, +# which is useful if you want to have a copy of the project side by side with this one. + +# type: # asterios, backdrop, cakephp, codeigniter, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, drupal12, generic, joomla, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress, wp-bedrock +# See https://docs.ddev.com/en/stable/users/quickstart/ for more +# information on the different project types + +# docroot: # Relative path to the directory containing index.php. + +# php_version: "8.4" # PHP version to use, "5.6" through "8.5" + +# You can explicitly specify the webimage but this +# is not recommended, as the images are often closely tied to DDEV's behavior, +# so this can break upgrades. + +# webimage: +# It’s unusual to change this option, and we don’t recommend it without Docker experience and a good reason. +# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.* + +# database: +# type: # mysql, mariadb, postgres +# version: # database version, like "10.11" or "8.0" +# MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8, 12.3 +# MySQL versions can be 5.5-8.0, 8.4 +# PostgreSQL versions can be 9-18 + +# router_http_port: # Port to be used for http (defaults to global configuration, usually 80) +# router_https_port: # Port for https (defaults to global configuration, usually 443) + +# xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart" +# Note that for most people the commands +# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, +# as leaving Xdebug enabled all the time is a big performance hit. + +# xhgui_http_port: "8143" +# xhgui_https_port: "8142" +# The XHGui ports can be changed from the default 8143 and 8142 +# Very rarely used + +# host_xhgui_port: "8142" +# Can be used to change the host binding port of the XHGui +# application. Rarely used; only when port conflict and +# bind_all_ports is used (normally with router disabled) + +# xhprof_mode: [prepend|xhgui|global] +# Default is "xhgui" + +# webserver_type: nginx-fpm, apache-fpm, generic + +# timezone: Europe/Berlin +# If timezone is unset, DDEV will attempt to derive it from the host system timezone +# using the $TZ environment variable or the /etc/localtime symlink. +# This is the timezone used in the containers and by PHP; +# it can be set to any valid timezone, +# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +# For example Europe/Dublin or MST7MDT + +# composer_root: +# Relative path to the Composer root directory from the project root. This is +# the directory which contains the composer.json and where all Composer related +# commands are executed. + +# composer_version: "2" +# You can set it to "" or "2" (default) for Composer v2 +# to use the latest major version available at the time your container is built. +# It is also possible to use any other Composer version channel. This includes: +# - 2.2 (latest Composer LTS version) +# - stable +# - preview +# - snapshot +# Alternatively, an explicit Composer version may be specified, for example "2.2.18". +# To reinstall Composer after the image was built, run "ddev utility rebuild". + +# nodejs_version: "24" +# change from the default system Node.js version to any other version. +# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information +# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation. + +# corepack_enable: false +# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm + +# additional_hostnames: +# - somename +# - someothername +# would provide http and https URLs for "somename.ddev.site" +# and "someothername.ddev.site". + +# additional_fqdns: +# - example.com +# - sub1.example.com +# would provide http and https URLs for "example.com" and "sub1.example.com" +# Please take care with this because it can cause great confusion. + +# upload_dirs: "custom/upload/dir" +# +# upload_dirs: +# - custom/upload/dir +# - ../private +# +# would set the destination paths for ddev import-files to /custom/upload/dir +# When Mutagen is enabled this path is bind-mounted so that all the files +# in the upload_dirs don't have to be synced into Mutagen. + +# disable_upload_dirs_warning: false +# If true, turns off the normal warning that says +# "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set" + +# ddev_version_constraint: "" +# Example: +# ddev_version_constraint: ">= 1.24.8" +# This will enforce that the running ddev version is within this constraint. +# See https://github.com/Masterminds/semver#checking-version-constraints for +# supported constraint formats + +# working_dir: +# web: /var/www/html +# db: /home +# would set the default working directory for the web and db services. +# These values specify the destination directory for ddev ssh and the +# directory in which commands passed into ddev exec are run. + +# omit_containers: [db, ddev-ssh-agent] +# Currently only these containers are supported. Some containers can also be +# omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit +# the "db" container, several standard features of DDEV that access the +# database container will be unusable. In the global configuration it is also +# possible to omit ddev-router, but not here. + +# performance_mode: "global" +# DDEV offers performance optimization strategies to improve the filesystem +# performance depending on your host system. Should be configured globally. +# +# If set, will override the global config. Possible values are: +# - "global": uses the value from the global config. +# - "none": disables performance optimization for this project. +# - "mutagen": enables Mutagen for this project. +# +# See https://docs.ddev.com/en/stable/users/install/performance/#mutagen + +# fail_on_hook_fail: False +# Decide whether 'ddev start' should be interrupted by a failing hook + +# host_https_port: "59002" +# The host port binding for https can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_webserver_port: "59001" +# The host port binding for the ddev-webserver can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_db_port: "59002" +# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic +# unless explicitly specified. + +# mailpit_http_port: "8025" +# mailpit_https_port: "8026" +# The Mailpit ports can be changed from the default 8025 and 8026 + +# host_mailpit_port: "8025" +# The mailpit port is not normally bound on the host at all, instead being routed +# through ddev-router, but it can be bound directly to localhost if specified here. + +# webimage_extra_packages: ['php${DDEV_PHP_VERSION}-tidy', 'php${DDEV_PHP_VERSION}-yac'] +# Extra Debian packages that are needed in the webimage can be added here + +# dbimage_extra_packages: [netcat, telnet, sudo] +# Extra Debian packages that are needed in the dbimage can be added here + +# use_dns_when_possible: true +# If the host has internet access and the domain configured can +# successfully be looked up, DNS will be used for hostname resolution +# instead of editing /etc/hosts +# Defaults to true + +# project_tld: ddev.site +# The top-level domain used for project URLs +# The default "ddev.site" allows DNS lookup via a wildcard + +# share_default_provider: ngrok +# The default share provider to use for "ddev share" +# Defaults to global configuration, usually "ngrok" +# Can be "ngrok" or "cloudflared" or the name of a custom provider from .ddev/share-providers/ + +# share_provider_args: --basic-auth username:pass1234 +# Provide extra flags to the share provider script +# See https://docs.ddev.com/en/stable/users/configuration/config/#share_provider_args + +# disable_settings_management: false +# If true, DDEV will not create CMS-specific settings files like +# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php +# In this case the user must provide all such settings. + +# You can inject environment variables into the web container with: +# web_environment: +# - SOMEENV=somevalue +# - SOMEOTHERENV=someothervalue + +# no_project_mount: false +# (Experimental) If true, DDEV will not mount the project into the web container; +# the user is responsible for mounting it manually or via a script. +# This is to enable experimentation with alternate file mounting strategies. +# For advanced users only! + +# bind_all_interfaces: false +# If true, host ports will be bound on all network interfaces, +# not the localhost interface only. This means that ports +# will be available on the local network if the host firewall +# allows it. + +# default_container_timeout: 120 +# The default time that DDEV waits for all containers to become ready can be increased from +# the default 120. This helps in importing huge databases, for example. + +#web_extra_exposed_ports: +#- name: nodejs +# container_port: 3000 +# http_port: 2999 +# https_port: 3000 +#- name: something +# container_port: 4000 +# https_port: 4000 +# http_port: 3999 +# Allows a set of extra ports to be exposed via ddev-router +# Fill in all three fields even if you don’t intend to use the https_port! +# If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start. +# +# The port behavior on the ddev-webserver must be arranged separately, for example +# using web_extra_daemons. +# For example, with a web app on port 3000 inside the container, this config would +# expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 +# web_extra_exposed_ports: +# - name: myapp +# container_port: 3000 +# http_port: 9998 +# https_port: 9999 + +#web_extra_daemons: +#- name: "http-1" +# command: "/var/www/html/node_modules/.bin/http-server -p 3000" +# directory: /var/www/html +#- name: "http-2" +# command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000" +# directory: /var/www/html + +# override_config: false +# By default, config.*.yaml files are *merged* into the configuration +# But this means that some things can't be overridden +# For example, if you have 'use_dns_when_possible: true' you can't override it with a merge +# and you can't erase existing hooks or all environment variables. +# However, with "override_config: true" in a particular config.*.yaml file, +# 'use_dns_when_possible: false' can override the existing values, and +# hooks: +# post-start: [] +# or +# web_environment: [] +# or +# additional_hostnames: [] +# can have their intended effect. 'override_config' affects only behavior of the +# config.*.yaml file it exists in. + +# Many DDEV commands can be extended to run tasks before or after the +# DDEV command is executed, for example "post-start", "post-import-db", +# "pre-composer", "post-composer" +# See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more +# information on the commands that can be extended and the tasks you can define +# for them. Example: +#hooks: +## Un-comment to consume async message. +# post-start: +# - exec: symfony run --daemon --watch=config,src,templates,vendor bin/console messenger:consume async -vv diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6699076 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[{compose.yaml,compose.*.yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.env b/.env new file mode 100644 index 0000000..9e19457 --- /dev/null +++ b/.env @@ -0,0 +1,27 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET= +APP_SHARE_DIR=var/share +###< symfony/framework-bundle ### + +###> symfony/routing ### +# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. +# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands +DEFAULT_URI=http://localhost +###< symfony/routing ### diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..03f06e2 --- /dev/null +++ b/.env.dev @@ -0,0 +1,4 @@ + +###> symfony/framework-bundle ### +APP_SECRET=c28dc9f45cdb321c0a49317ec3823c59 +###< symfony/framework-bundle ### diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a67f91e --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### diff --git a/README.md b/README.md index 853c075..fa913b3 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ # Enter + +We use [DDEV](https://ddev.com/) for development: + +``` shell +ddev start +ddev launch +``` diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..d8d530e --- /dev/null +++ b/bin/console @@ -0,0 +1,21 @@ +#!/usr/bin/env php +=8.4", + "ext-ctype": "*", + "ext-iconv": "*", + "symfony/console": "8.1.*", + "symfony/dotenv": "8.1.*", + "symfony/flex": "^2", + "symfony/framework-bundle": "8.1.*", + "symfony/runtime": "8.1.*", + "symfony/yaml": "8.1.*" + }, + "require-dev": { + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "bump-after-update": true, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*", + "symfony/polyfill-php83": "*", + "symfony/polyfill-php84": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "8.1.*" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..2ec33ce --- /dev/null +++ b/composer.lock @@ -0,0 +1,2608 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "8f2d2bb175ffe409da49e7034d1f3a93", + "packages": [ + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/cache", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "c12be586c0e4798e649e7501aa9181ddc9d93310" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/c12be586c0e4798e649e7501aa9181ddc9d93310", + "reference": "c12be586c0e4798e649e7501aa9181ddc9d93310", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^8.1" + }, + "conflict": { + "ext-redis": "<6.1", + "ext-relay": "<0.12.1" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "^1.0.3", + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-19T08:32:28+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/config", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "ec711a6c14ae287d9618fbd2c9de4e223a1a4b02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/ec711a6c14ae287d9618fbd2c9de4e223a1a4b02", + "reference": "ec711a6c14ae287d9618fbd2c9de4e223a1a4b02", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-20T09:59:12+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d07c06839e33047e2c894a6793248f3fb66c8129" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d07c06839e33047e2c894a6793248f3fb66c8129", + "reference": "d07c06839e33047e2c894a6793248f3fb66c8129", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T14:29:57+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "e79d512848b75f92374e473f8e9ead4202c965c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e79d512848b75f92374e473f8e9ead4202c965c5", + "reference": "e79d512848b75f92374e473f8e9ead4202c965c5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^8.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "4ea87b35c75f7052309ec9735c0eb5c1fd7d2182" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/4ea87b35c75f7052309ec9735c0eb5c1fd7d2182", + "reference": "4ea87b35c75f7052309ec9735c0eb5c1fd7d2182", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-26T10:31:34+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8b2a4289ffe5e2dc8fcf645b8e7870e1fa0325ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8b2a4289ffe5e2dc8fcf645b8e7870e1fa0325ce", + "reference": "8b2a4289ffe5e2dc8fcf645b8e7870e1fa0325ce", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7458da64220376b2e0dc2d8451bf43382c1ad297", + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "6b2f4a0eeb28b5d74f90862592923a654bc629b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6b2f4a0eeb28b5d74f90862592923a654bc629b3", + "reference": "6b2f4a0eeb28b5d74f90862592923a654bc629b3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:16:08+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8d7acede2b2ae07605783d1c43e49b5767036474" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8d7acede2b2ae07605783d1c43e49b5767036474", + "reference": "8d7acede2b2ae07605783d1c43e49b5767036474", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:16:08+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/4a6d98eea3ebc7f68d82810cb682eedca2649e99", + "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.1" + }, + "conflict": { + "composer/semver": "<1.7.2", + "symfony/dotenv": "<5.4" + }, + "require-dev": { + "composer/composer": "^2.1", + "phpunit/phpunit": "^12.4", + "symfony/dotenv": "^6.4.41|^7.4.13|^8.0.13", + "symfony/filesystem": "^6.4|^7.4|^8.0", + "symfony/process": "^6.4|^7.4|^8.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.11.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T17:25:22+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "1c7b41364312c67376a560e35c7f7f0be0c1b4ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/1c7b41364312c67376a560e35c7f7f0be0c1b4ea", + "reference": "1c7b41364312c67376a560e35c7f7f0be0c1b4ea", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.4.1", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^8.1.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^8.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.33", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^3.7.1", + "symfony/var-exporter": "^8.1" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/console": "<8.1.2", + "symfony/form": "<7.4", + "symfony/json-streamer": "<7.4", + "symfony/mailer": "<7.4.17|>=8.0,<8.1.5", + "symfony/messenger": "<7.4.10|>=8.0,<8.0.10", + "symfony/mime": "<7.4.9|>=8.0,<8.0.9", + "symfony/security-csrf": "<7.4", + "symfony/serializer": "<7.4", + "symfony/translation": "<7.4", + "symfony/webhook": "<7.4", + "symfony/workflow": "<7.4" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^7.4|^8.0", + "symfony/asset-mapper": "^7.4|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/console": "^8.1.2", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/dotenv": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/json-streamer": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/mailer": "^7.4|^8.0", + "symfony/messenger": "^7.4.10|^8.0.10", + "symfony/mime": "^7.4.9|^8.0.9", + "symfony/notifier": "^7.4|^8.0", + "symfony/object-mapper": "^7.4.9|^8.0.9", + "symfony/polyfill-intl-icu": "^1.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/scheduler": "^7.4|^8.0", + "symfony/security-bundle": "^7.4|^8.0", + "symfony/semaphore": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0", + "symfony/type-info": "^7.4.1|^8.0.1", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^7.4|^8.0", + "symfony/webhook": "^7.4|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T19:39:55+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee16f97e95cfa011a742714d7c8c8f70fe7423f4", + "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-20T09:59:12+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/0306e1e65b90023fe40de6c6be95d06396bcb2e6", + "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/serializer": "<7.4.15|>=8.0,<8.0.15|>=8.1,<8.1.2", + "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-22T13:45:00+00:00" + }, + { + "name": "symfony/polyfill-deepclone", + "version": "v1.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-deepclone.git", + "reference": "70ba0627efc68e97ea392843458a2dd9d6dbd156" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/70ba0627efc68e97ea392843458a2dd9d6dbd156", + "reference": "70ba0627efc68e97ea392843458a2dd9d6dbd156", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "provide": { + "ext-deepclone": "*" + }, + "suggest": { + "ext-deepclone": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\DeepClone\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the deepclone extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "deepclone", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.42.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T06:33:24+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T08:25:59+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T06:33:24+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-01T12:47:55+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3c188091b6b4fa2e4bc83a135caede12deb8576c", + "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-17T13:18:34+00:00" + }, + { + "name": "symfony/runtime", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/b7ea1abe04561e814b3134db0f56c287cedb35cc", + "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.4.1" + }, + "conflict": { + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "composer/composer": "^2.6", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dotenv": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Enables decoupling PHP applications from global state", + "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], + "support": { + "source": "https://github.com/symfony/runtime/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T09:55:08+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T07:35:25+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "61743d9bc7ab23b194527ca1be2fafd7dc93b74a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/61743d9bc7ab23b194527ca1be2fafd7dc93b74a", + "reference": "61743d9bc7ab23b194527ca1be2fafd7dc93b74a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:16:08+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", + "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-deepclone": "^1.40" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to export, instantiate, hydrate, clone and lazy-load PHP objects", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "deep-clone", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-13T16:11:28+00:00" + }, + { + "name": "symfony/yaml", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", + "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:16:08+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.4", + "ext-ctype": "*", + "ext-iconv": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.9.0" +} diff --git a/config/bundles.php b/config/bundles.php new file mode 100644 index 0000000..49d3fb6 --- /dev/null +++ b/config/bundles.php @@ -0,0 +1,5 @@ + ['all' => true], +]; diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml new file mode 100644 index 0000000..6899b72 --- /dev/null +++ b/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml new file mode 100644 index 0000000..7e1ee1f --- /dev/null +++ b/config/packages/framework.yaml @@ -0,0 +1,15 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + + # Note that the session will be started ONLY if you read or write from it. + session: true + + #esi: true + #fragments: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml new file mode 100644 index 0000000..0f34f87 --- /dev/null +++ b/config/packages/routing.yaml @@ -0,0 +1,10 @@ +framework: + router: + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + default_uri: '%env(DEFAULT_URI)%' + +when@prod: + framework: + router: + strict_requirements: null diff --git a/config/preload.php b/config/preload.php new file mode 100644 index 0000000..5ebcdb2 --- /dev/null +++ b/config/preload.php @@ -0,0 +1,5 @@ + [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|Param|null>|Param|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decorates_tag?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: CallbackType, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * decorates?: string, + * decorates_tag?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: array, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type FrameworkConfig = array{ + * secret?: scalar|Param|null, + * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false + * allowed_http_method_override?: null|list, + * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" + * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" + * test?: bool|Param, + * default_locale?: scalar|Param|null, // Default: "en" + * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false + * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false + * enabled_locales?: list, + * trusted_hosts?: Param|string|list, + * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] + * trusted_headers?: Param|string|list, + * error_controller?: scalar|Param|null, // Default: "error_controller" + * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * stateless_token_ids?: list, + * check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false + * cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" + * }, + * form?: bool|array{ // Form configuration + * enabled?: bool|Param, // Default: false + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * token_id?: scalar|Param|null, // Default: null + * field_name?: scalar|Param|null, // Default: "_token" + * field_attr?: array, + * }, + * }, + * http_cache?: bool|array{ // HTTP cache configuration + * enabled?: bool|Param, // Default: false + * debug?: bool|Param, // Default: "%kernel.debug%" + * trace_level?: "none"|"short"|"full"|Param, + * trace_header?: scalar|Param|null, + * default_ttl?: int|Param, + * private_headers?: list, + * skip_response_headers?: list, + * allow_reload?: bool|Param, + * allow_revalidate?: bool|Param, + * stale_while_revalidate?: int|Param, + * stale_if_error?: int|Param, + * terminate_on_cache_hit?: bool|Param, // Deprecated: Setting the "framework.http_cache.terminate_on_cache_hit.terminate_on_cache_hit" configuration option is deprecated. It will be removed in version 9.0. + * }, + * esi?: bool|array{ // ESI configuration + * enabled?: bool|Param, // Default: false + * }, + * ssi?: bool|array{ // SSI configuration + * enabled?: bool|Param, // Default: false + * }, + * fragments?: bool|array{ // Fragments configuration + * enabled?: bool|Param, // Default: false + * hinclude_default_template?: scalar|Param|null, // Default: null + * path?: scalar|Param|null, // Default: "/_fragment" + * }, + * profiler?: bool|array{ // Profiler configuration + * enabled?: bool|Param, // Default: false + * collect?: bool|Param, // Default: true + * collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null + * only_exceptions?: bool|Param, // Default: false + * only_main_requests?: bool|Param, // Default: false + * dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler" + * collect_serializer_data?: true|Param, // Deprecated: Setting the "framework.profiler.collect_serializer_data.collect_serializer_data" configuration option is deprecated. It will be removed in version 9.0. // Default: true + * }, + * workflows?: bool|array{ + * enabled?: bool|Param, // Default: false + * workflows?: array, + * definition_validators?: list, + * support_strategy?: scalar|Param|null, + * initial_marking?: \BackedEnum|Param|string|list, + * events_to_dispatch?: null|list, + * places?: Param|string|list, + * }>, + * transitions?: list, + * to?: \BackedEnum|Param|string|list, + * weight?: int|Param, // Default: 1 + * metadata?: array, + * }>, + * metadata?: array, + * }>, + * }, + * router?: bool|array{ // Router configuration + * enabled?: bool|Param, // Default: false + * resource?: scalar|Param|null, + * type?: scalar|Param|null, + * default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null + * http_port?: scalar|Param|null, // Default: 80 + * https_port?: scalar|Param|null, // Default: 443 + * strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true + * utf8?: bool|Param, // Default: true + * }, + * session?: bool|array{ // Session configuration + * enabled?: bool|Param, // Default: false + * storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native" + * handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. + * name?: scalar|Param|null, + * cookie_lifetime?: scalar|Param|null, + * cookie_path?: scalar|Param|null, + * cookie_domain?: scalar|Param|null, + * cookie_secure?: true|false|"auto"|Param, // Default: "auto" + * cookie_httponly?: bool|Param, // Default: true + * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * use_cookies?: bool|Param, + * gc_divisor?: scalar|Param|null, + * gc_probability?: scalar|Param|null, + * gc_maxlifetime?: scalar|Param|null, + * save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. + * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0 + * }, + * request?: bool|array{ // Request configuration + * enabled?: bool|Param, // Default: false + * formats?: array>, + * }, + * assets?: bool|array{ // Assets configuration + * enabled?: bool|Param, // Default: false + * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|Param|null, // Default: null + * version?: scalar|Param|null, // Default: null + * version_format?: scalar|Param|null, // Default: "%%s?%%s" + * json_manifest_path?: scalar|Param|null, // Default: null + * base_path?: scalar|Param|null, // Default: "" + * base_urls?: Param|string|list, + * packages?: array, + * }>, + * }, + * asset_mapper?: bool|array{ // Asset Mapper configuration + * enabled?: bool|Param, // Default: false + * paths?: Param|string|array, + * excluded_patterns?: list, + * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true + * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true + * public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" + * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" + * extensions?: array, + * importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" + * importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" + * importmap_script_attributes?: array, + * vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" + * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. + * enabled?: bool|Param, // Default: false + * formats?: list, + * extensions?: list, + * }, + * }, + * translator?: bool|array{ // Translator configuration + * enabled?: bool|Param, // Default: false + * fallbacks?: Param|string|list, + * logging?: bool|Param, // Default: false + * formatter?: scalar|Param|null, // Default: "translator.formatter.default" + * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" + * default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" + * paths?: list, + * pseudo_localization?: bool|array{ + * enabled?: bool|Param, // Default: false + * accents?: bool|Param, // Default: true + * expansion_factor?: float|Param, // Default: 1.0 + * brackets?: bool|Param, // Default: true + * parse_html?: bool|Param, // Default: false + * localizable_html_attributes?: list, + * }, + * providers?: array, + * locales?: list, + * }>, + * globals?: array, + * domain?: string|Param, + * }>, + * }, + * validation?: bool|array{ // Validation configuration + * enabled?: bool|Param, // Default: false + * enable_attributes?: bool|Param, // Default: true + * static_method?: Param|string|list, + * translation_domain?: scalar|Param|null, // Default: "validators" + * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|Param, // Default: "html5" + * mapping?: array{ + * paths?: list, + * }, + * not_compromised_password?: bool|array{ + * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true + * endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null + * }, + * disable_translation?: bool|Param, // Default: false + * property_metadata_existence_check?: bool|Param, // When enabled, validateProperty() and validatePropertyValue() throw an exception if no metadata is found for the given property. // Default: false + * auto_mapping?: array, + * }>, + * }, + * serializer?: bool|array{ // Serializer configuration + * enabled?: bool|Param, // Default: false + * enable_attributes?: bool|Param, // Default: true + * name_converter?: scalar|Param|null, + * circular_reference_handler?: scalar|Param|null, + * max_depth_handler?: scalar|Param|null, + * mapping?: array{ + * paths?: list, + * }, + * default_context?: array, + * named_serializers?: array, + * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true + * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true + * }>, + * }, + * property_access?: bool|array{ // Property access configuration + * enabled?: bool|Param, // Default: false + * magic_call?: bool|Param, // Default: false + * magic_get?: bool|Param, // Default: true + * magic_set?: bool|Param, // Default: true + * throw_exception_on_invalid_index?: bool|Param, // Default: false + * throw_exception_on_invalid_property_path?: bool|Param, // Default: true + * }, + * type_info?: bool|array{ // Type info configuration + * enabled?: bool|Param, // Default: false + * aliases?: array, + * }, + * property_info?: bool|array{ // Property info configuration + * enabled?: bool|Param, // Default: false + * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. // Default: true + * }, + * cache?: array{ // Cache configuration + * prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" + * app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem" + * system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system" + * directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app" + * default_psr6_provider?: scalar|Param|null, + * default_redis_provider?: scalar|Param|null, // Default: "redis://localhost" + * default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost" + * default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost" + * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" + * default_pdo_provider?: scalar|Param|null, // Default: null + * pools?: array, + * tags?: scalar|Param|null, // Default: null + * public?: bool|Param, // Default: false + * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. + * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|Param|null, + * clearer?: scalar|Param|null, + * marshaller?: scalar|Param|null, // The marshaller service to use for this pool. + * }>, + * }, + * php_errors?: array{ // PHP errors handling configuration + * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true + * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true + * }, + * exceptions?: array, + * web_link?: bool|array{ // Web links configuration + * enabled?: bool|Param, // Default: false + * }, + * lock?: Param|bool|string|array{ // Lock configuration + * enabled?: bool|Param, // Default: false + * resources?: Param|string|array>, + * }, + * semaphore?: Param|bool|string|array{ // Semaphore configuration + * enabled?: bool|Param, // Default: false + * resources?: Param|string|array, + * }, + * messenger?: bool|array{ // Messenger configuration + * enabled?: bool|Param, // Default: false + * routing?: array>, + * serializer?: array{ + * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" + * symfony_serializer?: array{ + * format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" + * context?: array, + * }, + * }, + * transports?: array, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: Param|string|array{ + * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 + * }, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null + * }>, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * stop_worker_on_signals?: Param|int|string|list, + * default_bus?: scalar|Param|null, // Default: null + * buses?: array, + * }>, + * }>, + * }, + * scheduler?: bool|array{ // Scheduler configuration + * enabled?: bool|Param, // Default: false + * }, + * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true + * http_client?: bool|array{ // HTTP Client configuration + * enabled?: bool|Param, // Default: false + * max_host_connections?: int|Param, // The maximum number of connections to a single host. + * default_options?: array{ + * headers?: array, + * vars?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...) + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. // Default: 86400 + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: Param|int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }, + * mock_response_factory?: scalar|Param|null, // `true` to always return empty 200 responses, or the id of the service to use to generate mock responses - which should be either an invokable or an iterable. + * scoped_clients?: array, + * headers?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * mock_response_factory?: scalar|Param|null, // `true` to always return empty 200 responses, `false` to disable mocking, or the id of the service to use to generate mock responses (invokable or iterable). + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. // Default: 86400 + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: Param|int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }>, + * }, + * mailer?: bool|array{ // Mailer configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * dsn?: scalar|Param|null, // Default: null + * transports?: array, + * envelope?: array{ // Mailer Envelope configuration + * sender?: scalar|Param|null, + * recipients?: Param|string|list, + * allowed_recipients?: Param|string|list, + * }, + * headers?: array, + * dkim_signer?: bool|array{ // DKIM signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" + * domain?: scalar|Param|null, // Default: "" + * select?: scalar|Param|null, // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: "" + * options?: array, + * }, + * smime_signer?: bool|array{ // S/MIME signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Path to key (in PEM format) // Default: "" + * certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: null + * extra_certificates?: scalar|Param|null, // Default: null + * sign_options?: int|Param, // Default: null + * }, + * smime_encrypter?: bool|array{ // S/MIME encrypter configuration + * enabled?: bool|Param, // Default: false + * repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" + * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null + * }, + * }, + * secrets?: bool|array{ + * enabled?: bool|Param, // Default: true + * vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" + * local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local" + * decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" + * }, + * notifier?: bool|array{ // Notifier configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * chatter_transports?: array, + * texter_transports?: array, + * notification_on_failed_messages?: bool|Param, // Default: false + * channel_policy?: array>, + * admin_recipients?: list, + * }, + * rate_limiter?: bool|array{ // Rate limiter configuration + * enabled?: bool|Param, // Default: false + * limiters?: array, + * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". + * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 + * }, + * anchor_at?: scalar|Param|null, // Aligns the "fixed_window" policy to a calendar (e.g. "2024-01-05 00:00:00 UTC" combined with `interval: 1 month` resets the counter on the 5th of each month). UTC if not specified. // Default: null + * }>, + * }, + * uid?: bool|array{ // Uid configuration + * enabled?: bool|Param, // Default: false + * default_uuid_version?: 7|6|4|1|Param, // Default: 7 + * name_based_uuid_version?: 5|3|Param, // Default: 5 + * name_based_uuid_namespace?: scalar|Param|null, + * time_based_uuid_version?: 7|6|1|Param, // Default: 7 + * time_based_uuid_node?: scalar|Param|null, + * uuid47_secret?: scalar|Param|null, // A high-entropy secret used by the "uuid47_transformer" service. Defaults to "kernel.secret". // Default: null + * }, + * html_sanitizer?: bool|array{ // HtmlSanitizer configuration + * enabled?: bool|Param, // Default: false + * sanitizers?: array, + * block_elements?: Param|string|list, + * drop_elements?: Param|string|list, + * allow_attributes?: array, + * drop_attributes?: array, + * force_attributes?: array>, + * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false + * allowed_link_schemes?: Param|string|list, + * allowed_link_hosts?: Param|null|string|list, + * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false + * allowed_media_schemes?: Param|string|list, + * allowed_media_hosts?: Param|null|string|list, + * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false + * with_attribute_sanitizers?: Param|string|list, + * without_attribute_sanitizers?: Param|string|list, + * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 + * }>, + * }, + * webhook?: bool|array{ // Webhook configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" + * event_header_name?: scalar|Param|null, // Default: "Webhook-Event" + * id_header_name?: scalar|Param|null, // Default: "Webhook-Id" + * signature_header_name?: scalar|Param|null, // Default: "Webhook-Signature" + * signing_algorithm?: scalar|Param|null, // Default: "sha256" + * routing?: array, + * }, + * remote-event?: bool|array{ // RemoteEvent configuration + * enabled?: bool|Param, // Default: false + * }, + * json_streamer?: bool|array{ // JSON streamer configuration + * enabled?: bool|Param, // Default: false + * default_options?: array{ + * include_null_properties?: bool|Param, // Encode the properties with null value // Default: false + * ... + * }, + * }, + * } + * @psalm-type ConfigType = array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * }, + * ..., + * }> + * } + */ +final class App +{ + /** + * @param ConfigType $config + * + * @psalm-return ConfigType + */ + public static function config(array $config): array + { + /** @var ConfigType $config */ + $config = AppReference::config($config); + + return $config; + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/config/routes.yaml b/config/routes.yaml new file mode 100644 index 0000000..cef258c --- /dev/null +++ b/config/routes.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json + +# This file is the entry point to configure the routes of your app. +# Methods with the #[Route] attribute are automatically imported. +# See also https://symfony.com/doc/current/routing.html + +# To list all registered routes, run the following command: +# bin/console debug:router + +controllers: + resource: routing.controllers diff --git a/config/routes/framework.yaml b/config/routes/framework.yaml new file mode 100644 index 0000000..bc1feac --- /dev/null +++ b/config/routes/framework.yaml @@ -0,0 +1,4 @@ +when@dev: + _errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.php' + prefix: /_error diff --git a/config/services.yaml b/config/services.yaml new file mode 100644 index 0000000..79b8ce2 --- /dev/null +++ b/config/services.yaml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json + +# This file is the entry point to configure your own services. +# Files in the packages/ subdirectory configure your dependencies. +# See also https://symfony.com/doc/current/service_container/import.html + +# Put parameters here that don't need to change on each machine where the app is deployed +# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration +parameters: + +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/' + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..c0037a8 --- /dev/null +++ b/public/index.php @@ -0,0 +1,9 @@ + An array of allowed values for APP_ENV + */ + private function getAllowedEnvs(): array + { + return ['prod', 'dev', 'test']; + } +} diff --git a/symfony.lock b/symfony.lock new file mode 100644 index 0000000..f0ee42d --- /dev/null +++ b/symfony.lock @@ -0,0 +1,60 @@ +{ + "symfony/console": { + "version": "8.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461" + }, + "files": [ + "bin/console" + ] + }, + "symfony/flex": { + "version": "2.11", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.4", + "ref": "52e9754527a15e2b79d9a610f98185a1fe46622a" + }, + "files": [ + ".env", + ".env.dev" + ] + }, + "symfony/framework-bundle": { + "version": "8.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "8.1", + "ref": "312027aea160796a50bf2d185503afdb5d71f570" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php", + ".editorconfig" + ] + }, + "symfony/routing": { + "version": "8.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.4", + "ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + } +} From d97f3da5f18d7c040340808b7c8a61558d9a42e8 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 31 Aug 2026 10:21:44 +0200 Subject: [PATCH 003/101] =?UTF-8?q?Experiments=20=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ddev/config.yaml | 1 + .ddev/docker-compose.scorpio.yaml | 37 + .env | 4 + .env.test | 3 + .gitignore | 25 + .php-cs-fixer.dist.php | 20 + .twig-cs-fixer.dist.php | 16 + Notes.md | 9 - README.md | 153 +- Taskfile.yml | 178 + assets/app.js | 114 + assets/styles/app.css | 0 bin/phpunit | 4 + composer.json | 108 +- composer.lock | 7106 +++++++++++++++++++++++-- config/bundles.php | 5 + config/packages/asset_mapper.yaml | 11 + config/packages/debug.yaml | 5 + config/packages/framework.yaml | 8 + config/packages/property_info.yaml | 3 + config/packages/twig.yaml | 6 + config/packages/web_profiler.yaml | 11 + config/reference.php | 126 +- config/routes/web_profiler.yaml | 8 + importmap.php | 28 + phpstan.dist.neon | 20 + phpunit.dist.xml | 44 + src/Command/BrokerEntityDelete.php | 96 + src/Command/BrokerImportGeoJson.php | 101 + src/Command/ConvertPointCommand.php | 46 + src/Controller/DataController.php | 65 + src/Controller/DefaultController.php | 18 + src/GeometryHelper.php | 85 + src/GeometryHelper/Crs.php | 15 + symfony.lock | 134 + templates/base.html.twig | 39 + templates/default/hundeskov.html.twig | 113 + templates/default/index.html.twig | 63 + tests/GeometryHelperTest.php | 61 + tests/bootstrap.php | 13 + 40 files changed, 8382 insertions(+), 520 deletions(-) create mode 100644 .ddev/docker-compose.scorpio.yaml create mode 100644 .env.test create mode 100644 .php-cs-fixer.dist.php create mode 100644 .twig-cs-fixer.dist.php delete mode 100644 Notes.md create mode 100644 Taskfile.yml create mode 100644 assets/app.js create mode 100644 assets/styles/app.css create mode 100755 bin/phpunit create mode 100644 config/packages/asset_mapper.yaml create mode 100644 config/packages/debug.yaml create mode 100644 config/packages/property_info.yaml create mode 100644 config/packages/twig.yaml create mode 100644 config/packages/web_profiler.yaml create mode 100644 config/routes/web_profiler.yaml create mode 100644 importmap.php create mode 100644 phpstan.dist.neon create mode 100644 phpunit.dist.xml create mode 100644 src/Command/BrokerEntityDelete.php create mode 100644 src/Command/BrokerImportGeoJson.php create mode 100644 src/Command/ConvertPointCommand.php create mode 100644 src/Controller/DataController.php create mode 100644 src/Controller/DefaultController.php create mode 100644 src/GeometryHelper.php create mode 100644 src/GeometryHelper/Crs.php create mode 100644 templates/base.html.twig create mode 100644 templates/default/hundeskov.html.twig create mode 100644 templates/default/index.html.twig create mode 100644 tests/GeometryHelperTest.php create mode 100644 tests/bootstrap.php diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 0594bc0..6ea79ab 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -14,6 +14,7 @@ composer_version: "2" web_environment: [] nodejs_version: "24" corepack_enable: false +disable_upload_dirs_warning: true # Key features of DDEV's config.yaml: diff --git a/.ddev/docker-compose.scorpio.yaml b/.ddev/docker-compose.scorpio.yaml new file mode 100644 index 0000000..e1f60a2 --- /dev/null +++ b/.ddev/docker-compose.scorpio.yaml @@ -0,0 +1,37 @@ +networks: + scorpio: + +# https://docs.ddev.com/en/stable/users/extend/custom-docker-services/ +services: + scorpio: + container_name: "ddev-${DDEV_SITENAME}-scorpio" + hostname: scorpio.local + image: scorpiobroker/all-in-one-runner:java-latest + restart: "no" + ports: + - "9090" + environment: + - VIRTUAL_HOST=${DDEV_HOSTNAME} + - HTTP_EXPOSE=9090:9999 + - DBHOST=scorpio-db + depends_on: + - "scorpio-db" + networks: + - default + - scorpio + platform: "linux/amd64" + + scorpio-db: + container_name: "ddev-${DDEV_SITENAME}-scorpio-db" + image: postgis/postgis + environment: + - POSTGRES_USER=ngb + - POSTGRES_PASSWORD=ngb + - POSTGRES_DB=ngb + networks: + - scorpio + volumes: + - ./data/scorpio-db:/var/lib/postgresql/data + platform: "linux/amd64" + # https://stackoverflow.com/a/58806511 + command: ["postgres", "-c", "log_statement=all"] diff --git a/.env b/.env index 9e19457..b9dbe2b 100644 --- a/.env +++ b/.env @@ -25,3 +25,7 @@ APP_SHARE_DIR=var/share # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands DEFAULT_URI=http://localhost ###< symfony/routing ### + +###> app ### +APP_BROKER_BASE_URI=http://scorpio.local:9090/ +###< app ### diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..64bd111 --- /dev/null +++ b/.env.test @@ -0,0 +1,3 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' diff --git a/.gitignore b/.gitignore index a67f91e..fbd3470 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.ddev/data +.idea ###> symfony/framework-bundle ### /.env.local @@ -8,3 +10,26 @@ /var/ /vendor/ ###< symfony/framework-bundle ### + +###> phpunit/phpunit ### +/phpunit.xml +/.phpunit.cache/ +###< phpunit/phpunit ### + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.php +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### + +###> vincentlanglet/twig-cs-fixer ### +/.twig-cs-fixer.cache +###< vincentlanglet/twig-cs-fixer ### + +###> phpstan/phpstan ### +phpstan.neon +###< phpstan/phpstan ### + +###> symfony/asset-mapper ### +/public/assets/ +/assets/vendor/ +###< symfony/asset-mapper ### diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..c23b927 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,20 @@ +in(__DIR__); +// … that are not ignored by VCS +$finder->ignoreVCSIgnored(true); + +$config = new PhpCsFixer\Config(); +$config->setFinder($finder); + +$config->setRules([ + '@Symfony' => true, +]); + +return $config; diff --git a/.twig-cs-fixer.dist.php b/.twig-cs-fixer.dist.php new file mode 100644 index 0000000..8242555 --- /dev/null +++ b/.twig-cs-fixer.dist.php @@ -0,0 +1,16 @@ +in(__DIR__); +// … that are not ignored by VCS +$finder->ignoreVCSIgnored(true); + +$config = new TwigCsFixer\Config\Config(); +$config->setFinder($finder); + +return $config; diff --git a/Notes.md b/Notes.md deleted file mode 100644 index bbd29b1..0000000 --- a/Notes.md +++ /dev/null @@ -1,9 +0,0 @@ -# Notes - -## GeoJSON - -* [New Features and Changes in NGSI-LD v1.3.1](https://docbox.etsi.org/ISG/CIM/Open/NGSI-LD_API_v1.3.1-New_Features_and_Changes.pdf) -* https://github.com/ScorpioBroker/ScorpioBroker/issues/128 - * https://github.com/ScorpioBroker/ScorpioBroker/issues/576 - -Is a `location` property the way to store location data on an entity? diff --git a/README.md b/README.md index fa913b3..8e132d2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,157 @@ # Enter -We use [DDEV](https://ddev.com/) for development: +We use [DDEV](https://ddev.com/) and [Task](https://taskfile.dev/) for development: ``` shell -ddev start +task site:install + +``` shell +task site:update ddev launch ``` + +## Broker + +A [Scorpio Broker](https://scorpio.readthedocs.io/) is part of the development setup. + +``` shell +ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/types | jq" +``` + +Load some example data: + +``` shell name=import-toilet +ddev console app:broker:entity:delete toilet +ddev console app:broker:import:geojson toilet 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter' +ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --get --data-urlencode type=toilet" | jq '.[]|with_entries(select([.key] | inside(["id", "type", "location"])))' +``` + +``` shell name=import-handicapparkering +ddev console app:broker:entity:delete handicapparkering +ddev console app:broker:import:geojson handicapparkering 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' +ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --get --data-urlencode type=handicapparkering" | jq '.[]|with_entries(select([.key] | inside(["id", "type", "location"])))' +``` + +``` shell name=import-hundeskov +ddev console app:broker:entity:delete hundeskov +ddev console app:broker:import:geojson hundeskov 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=hundeskove_friluftsliv_aarhus' +ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --get --data-urlencode type=hundeskov" | jq '.[]|with_entries(select([.key] | inside(["id", "type", "location", "geometry"])))' +``` + + +### Broker API request examples + + + +``` shell name=scorpio-entity-create substitutions="{«entity-type»: Room, «entity-id»: 'house2:smartrooms:room1'}" +ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --header 'content-type: application/json' --data @-" <<'JSON' +{ + "type": "«entity-type»", + "id": "«entity-id»" +} +JSON + + +# EPSG:4326?! +ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities/«entity-id»/attrs --header 'content-type: application/json' --data @-" <<'JSON' +{ + "location": { + "type": "geo:json", + "value": { + "type": "Point", + "coordinates": [ + 10.15711687080293, 56.126271111641266 + ] + } + } +} +JSON + + +# Get the entities +ddev exec "curl --silent --header 'accept: application/ld+json' http://scorpio.local:9090/ngsi-ld/v1/entities?type=«entity-type» | jq" + +# Get the entities as GeoJSON +ddev exec "curl --silent --header 'accept: application/geo+json' http://scorpio.local:9090/ngsi-ld/v1/entities?type=«entity-type» | jq" +``` + +> [!CAUTION] +> Excuse me what?! +> +> ``` shell +> ddev exec "curl --silent --header 'accept: application/geo+json' 'http://scorpio.local:9090/ngsi-ld/v1/entities?georel=near;maxDistance%3D%3D2000&geometry=Point&coordinates=%5B8,40%5D'" +> ddev exec "curl --silent --header 'accept: application/geo+json' 'http://scorpio.local:9090/ngsi-ld/v1/entities?georel=near;maxDistance==2000&geometry=Point&coordinates=%5B8,40%5D'" +> ddev exec "curl --silent --header 'accept: application/geo+json' 'http://scorpio.local:9090/ngsi-ld/v1/entities?georel=near;maxDistance==2000&geometry=Point&coordinates=[10,56]'" +> > ``` + +## GeoJSON + +* "[Position](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1)": `(longitude, latitude)` + * "[Coordinate Reference System](https://datatracker.ietf.org/doc/html/rfc7946#section-4)" + * But … + + : + + ```json + { + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": "EPSG:25832" + } + }, + "bbox": [563262.0903961, 6209995.05346621, 579587.800694027, 6231954.36230685], + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPoint", + "coordinates": [ + [576933.018139537, 6218035.22691216] + ] + }, + "properties": { + … + ``` + +* + + +--- + +* +* +* +* + +Must `location` be a `Point` in ngsi-ld? + +``` shell +ddev exec --service scorpio-db "psql ngb ngb" +``` + +``` sql +2026-08-30 10:26:19.789 UTC [41] LOG: execute 0000000: WITH D0 AS (SELECT ID, ENTITY, TRUE as PARENT FROM ENTITY WHERE ST_DWithin( location::geography, ST_SetSRID(ST_GeomFromGeoJSON('{"type": "Point", "coordinates": [10.236808047161853,56.101226991176155] }'), 4326)::geography, 2000.0) ORDER BY createdAt limit $1 offset $2) SELECT ID, ENTITY, PARENT FROM D0 +``` + + + +``` shell +ddev exec --service scorpio-db "psql ngb ngb" <<< "SELECT id , e_types, location FROM entity;" +``` + +``` shell name=hmm +ddev console app:import:geojson toilet 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter' \ + && ddev exec --service scorpio-db "psql ngb ngb" <<< "SELECT id , e_types, ST_AsText(location) AS location FROM entity WHERE id = 'toilet:0000';" \ + && ddev exec --service scorpio-db "psql ngb ngb" <<< "SELECT temporalentity_id, ST_AsText(location) AS location, ST_asText(geovalue) AS geovalue, createdat FROM temporalentityattrinstance WHERE temporalentity_id = 'toilet:0000' ORDER BY createdat DESC LIMIT 10;" +``` + + + +* [Public toilets in Aarhus Municipality](https://www.opendata.dk/city-of-aarhus/toiletter-i-aarhus-kommune) + * [Offentlige toiletter](http://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter): +* [Parking in Aarhus Municipality – Zones, Permits and Spaces](https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune) + * [Handicapparkering](https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap): + +* diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..400d1f1 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,178 @@ +version: '3' + +# https://taskfile.dev/usage/#env-files +dotenv: ['.env.local', '.env'] + +tasks: + site:install: + desc: Install site from scratch + prompt: Really (re)install site? + cmds: + - ddev delete --omit-snapshot --yes + - rm -fr .ddev/data/scorpio-db/ + - task: site:update + + site:update: + desc: Update site + cmds: + - ddev restart + - ddev composer install + # - ddev console doctrine:migrations:migrate --no-interaction + - ddev console cache:clear + + test: + desc: Run all tests + cmds: + - task: test:application + - task: test:integration + + test:integration: &test_task + env: + APP_ENV: test + desc: 'Run all {{.TASK | replace "test:" "" }} tests' + cmd: ddev php bin/phpunit tests/{{.TASK | replace "test:" "" | title }} + + test:application: *test_task + + coding-standards:apply: + desc: 'Apply coding standards' + cmds: + - task: coding-standards:assets:apply + - task: coding-standards:composer:apply + - task: coding-standards:markdown:apply + - task: coding-standards:php:apply + - task: coding-standards:twig:apply + - task: coding-standards:yaml:apply + + coding-standards:check: + desc: 'Apply coding standards' + cmds: + - task: coding-standards:assets:check + - task: coding-standards:composer:check + - task: coding-standards:markdown:check + - task: coding-standards:php:check + - task: coding-standards:twig:check + - task: coding-standards:yaml:check + + coding-standards:assets:apply: + desc: 'Apply coding standards for assets' + cmd: + task: compose + vars: + TASK_ARGS: run --rm prettier 'assets/' --write + + coding-standards:assets:check: + desc: 'Apply and check coding standards for assets' + cmds: + - task: coding-standards:assets:apply + - task: compose + vars: + TASK_ARGS: run --rm prettier 'assets/' --check + + coding-standards:composer:apply: + desc: 'Apply coding standards for Composer' + cmd: + task: composer + vars: + TASK_ARGS: normalize + + coding-standards:composer:check: + desc: 'Apply and check coding standards for Composer' + cmds: + - task: coding-standards:composer:apply + - task: composer + vars: + TASK_ARGS: audit + - task: composer + vars: + TASK_ARGS: normalize --dry-run + - task: composer + vars: + TASK_ARGS: validate --strict + + coding-standards:markdown:apply: + desc: 'Apply coding standards for Markdown' + cmds: + - task: compose + vars: + TASK_ARGS: run --rm markdownlint '**/*.md' --fix + + coding-standards:markdown:check: + desc: 'Apply and check coding standards for Markdown' + cmds: + - task: coding-standards:markdown:apply + - task: compose + vars: + TASK_ARGS: run --rm markdownlint '**/*.md' + + coding-standards:php:apply: + desc: 'Apply coding standards for PHP' + cmds: + - task: compose + vars: + TASK_ARGS: exec phpfpm vendor/bin/rector + - task: compose + vars: + TASK_ARGS: exec phpfpm vendor/bin/php-cs-fixer fix + + coding-standards:php:check: + desc: 'Apply and check coding standards for PHP' + cmds: + - task: coding-standards:php:apply + - task: compose + vars: + TASK_ARGS: exec phpfpm vendor/bin/rector --dry-run + - task: compose + vars: + TASK_ARGS: exec phpfpm vendor/bin/php-cs-fixer check + + coding-standards:twig:apply: + desc: 'Apply coding standards for Twig' + cmds: + - task: compose + vars: + TASK_ARGS: exec phpfpm vendor/bin/twig-cs-fixer fix + + coding-standards:twig:check: + desc: 'Apply and check coding standards for Twig' + cmds: + - task: coding-standards:twig:apply + - task: compose + vars: + TASK_ARGS: exec phpfpm vendor/bin/twig-cs-fixer check + + coding-standards:yaml:apply: + desc: 'Apply coding standards for YAML' + cmds: + - task: compose + vars: + TASK_ARGS: run --rm prettier --parser yaml phpstan*.neon '**/*.{yml,yaml}' --write + + coding-standards:yaml:check: + desc: 'Apply and check coding standards for YAML' + cmds: + - task: coding-standards:yaml:apply + - task: compose + vars: + TASK_ARGS: run --rm prettier --parser yaml phpstan*.neon '**/*.{yml,yaml}' --check + + code-analysis: + desc: 'Run code analysis' + cmds: + - task: code-analysis:phpstan + - task: code-analysis:mago + + code-analysis:phpstan: + desc: 'Run PHPStan' + cmds: + - task: compose + vars: + TASK_ARGS: run --rm --no-deps phpfpm composer install --no-scripts + - task: compose + vars: + TASK_ARGS: run --rm --no-deps phpfpm vendor/bin/phpstan + + default: + cmds: + - task --list-all + silent: true diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..d7879d7 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,114 @@ +/* + * Welcome to your app's main JavaScript file! + * + * This file will be included onto the page via the importmap() Twig function, + * which should already be in your base.html.twig. + */ +import './styles/app.css'; +import WidgetAPI from 'septima-widget' + +// const widget = new WidgetAPI('#map', { +// map: { +// debug: true, +// view: { +// zoomLevel: 4, +// "x": 572929.1565135007, +// "y": 6225655.880819891, +// // x: 724413, +// // y: 6175985, +// }, +// layer: [ +// { +// namedlayer: "#septima_standard" +// }, + +// // { +// // "features_host": "https://widget.cdn.septima.dk/demodata/dataapi1.json", +// // "visible": true, +// // "features": true, +// // "features_dataType": "json", +// // "type": "rawjson", +// // "features_template_rows": "0.produktionsenhed", +// // "features_template": "{ 'type': 'Feature', 'properties': { 'id': <%= cvrnummer %>, 'navn': '<%= navn %>'}, 'geometry': <%= _formatJSON(geometri) %>}", +// // "features_style": [ +// // { +// // "namedstyle": "#004", +// // "symbol": "circle", +// // "fillcolor": "rgba(255,255,255,1)", +// // "strokecolor": "<%= _getRandomColor(1, id) %>", +// // "strokewidth": 2, +// // "radius": 14 +// // }, +// // { +// // "namedstyle": "#004", +// // "fillcolor": "<%= _getRandomColor(1, id) %>", +// // "yOffset": 15, +// // "scale": 0.5, +// // "svg": "" +// // } +// // ] +// // }, + +// { +// "name": "Data", +// "selectable": true, +// "features": true, +// // # https://widget.cdn.septima.dk/latest/components/helpers/layer.html#type +// "type": "geojson", +// // # https://datatracker.ietf.org/doc/html/rfc7946#section-4 +// // # https://geoinfospot.com/what-is-wgs84-the-coordinate-system/ +// "srs": "EPSG:4326", +// "visible": true, + +// // # "features_host": "https://widget.cdn.septima.dk/demodata/skoler.geojson", + +// // # "features_host": "/data/ngsi-ld/v1/entities.geojson?type=Room", + +// // # "features_host": "/data/ngsi-ld/v1/entities.geojson?type=toilet", + +// // # "features_host": "/data/ngsi-ld/v1/entities.geojson?type=handicapparkering", + +// "features_host": "/data/ngsi-ld/v1/entities.geojson?type=hundeskov&limit=1", +// // # https://widget.cdn.septima.dk/latest/components/helpers/layer.html#features-template +// // # https://widget.cdn.septima.dk/latest/components/helpers/template.html#formatjson-data + +// // https://widget.cdn.septima.dk/latest/examples/data23.html#data-fra-json +// "features_host": "/data/ngsi-ld/v1/entities?type=hundeskov&limit=1", +// "features_dataType": "json", +// "type": "rawjson", +// "features_template": "{ 'type': 'Feature', 'properties': {'id': <%= cvrnummer %>, 'navn': '<%= navn %>'}, 'geometry': <%= _formatJSON(location) %>}", +// "features_template": "{ 'type': 'Feature', 'properties': { 'id': <%= cvrnummer %>, 'navn': '<%= navn %>'}, 'geometry': <%= _formatJSON(geometri) %>}", + +// // # "features_template": "{ 'type': 'Feature', 'properties': {\"navn\": \"Hmm …\"}, 'geometry': <%= _formatJSON('{ \"type\": \"Point\", \"coordinates\": [ 10.179484273715866, 56.18474125523462 ] }') %>}", + +// // "features_dataType": "json", +// // # "features_type": "Point", + +// "features_style": [ +// { +// "symbol": "circle", +// "symbol_selected": "circle", +// "radius": 15, +// "radius_selected": 20, +// "fillcolor": "rgba(1, 119, 134, 1)", +// "fillcolor_selected": "rgba(1, 119, 134, 1)" +// }, +// { +// "icon": "school", +// "icon_selected": "school", +// "scale": 0.8, +// "scale_selected": 1.2, +// "fillcolor": "rgba(255, 255, 255, 1)", +// "fillcolor_selected": "rgba(255, 255, 255, 1)", +// "xOffset": 0, +// "yOffset": 1, +// "yOffset_selected": 2 +// } +// ], + +// }, +// ], +// } +// }) + +// console.debug({widget}) diff --git a/assets/styles/app.css b/assets/styles/app.css new file mode 100644 index 0000000..e69de29 diff --git a/bin/phpunit b/bin/phpunit new file mode 100755 index 0000000..ac5eef1 --- /dev/null +++ b/bin/phpunit @@ -0,0 +1,4 @@ +#!/usr/bin/env php +=8.4", "ext-ctype": "*", "ext-iconv": "*", - "symfony/console": "8.1.*", - "symfony/dotenv": "8.1.*", + "itamair/geophp": "^1.12", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpdoc-parser": "^2.3", + "proj4php/proj4php": "^2.0", + "symfony/asset": "~8.1.0", + "symfony/asset-mapper": "~8.1.0", + "symfony/console": "~8.1.0", + "symfony/dotenv": "~8.1.0", "symfony/flex": "^2", - "symfony/framework-bundle": "8.1.*", - "symfony/runtime": "8.1.*", - "symfony/yaml": "8.1.*" + "symfony/framework-bundle": "~8.1.0", + "symfony/http-client": "~8.1.0", + "symfony/property-access": "~8.1.0", + "symfony/property-info": "~8.1.0", + "symfony/runtime": "~8.1.0", + "symfony/serializer": "~8.1.0", + "symfony/twig-bundle": "~8.1.0", + "symfony/yaml": "~8.1.0", + "twig/extra-bundle": "^2.12 || ^3.0", + "twig/twig": "^2.12 || ^3.0" }, "require-dev": { - }, - "config": { - "allow-plugins": { - "php-http/discovery": true, - "symfony/flex": true, - "symfony/runtime": true - }, - "bump-after-update": true, - "sort-packages": true - }, - "autoload": { - "psr-4": { - "App\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "App\\Tests\\": "tests/" - } + "ergebnis/composer-normalize": "^2.52", + "friendsofphp/php-cs-fixer": "^3.95", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^13.3", + "rector/rector": "^2.6", + "symfony/browser-kit": "~8.1.0", + "symfony/css-selector": "~8.1.0", + "symfony/debug-bundle": "8.1.*", + "symfony/maker-bundle": "^1.67", + "symfony/stopwatch": "~8.1.0", + "symfony/web-profiler-bundle": "~8.1.0", + "vincentlanglet/twig-cs-fixer": "^4.1" }, "replace": { "symfony/polyfill-ctype": "*", @@ -47,25 +51,49 @@ "symfony/polyfill-php83": "*", "symfony/polyfill-php84": "*" }, - "scripts": { - "auto-scripts": { - "cache:clear": "symfony-cmd", - "assets:install %PUBLIC_DIR%": "symfony-cmd" - }, - "post-install-cmd": [ - "@auto-scripts" - ], - "post-update-cmd": [ - "@auto-scripts" - ] - }, "conflict": { "symfony/symfony": "*" }, + "minimum-stability": "stable", + "prefer-stable": true, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "php-http/discovery": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "bump-after-update": true, + "sort-packages": true + }, "extra": { "symfony": { - "allow-contrib": false, + "allow-contrib": true, + "docker": false, "require": "8.1.*" } + }, + "scripts": { + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ], + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd", + "importmap:install": "symfony-cmd" + } } } diff --git a/composer.lock b/composer.lock index 2ec33ce..782be9a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,467 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8f2d2bb175ffe409da49e7034d1f3a93", + "content-hash": "5bd73ae1836b8ed57dcd7a8086881f80", "packages": [ + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "itamair/geophp", + "version": "1.12", + "source": { + "type": "git", + "url": "https://github.com/itamair/geoPHP.git", + "reference": "e0f87ad0e45f3898892a8f3bc02e8717c01554fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/itamair/geoPHP/zipball/e0f87ad0e45f3898892a8f3bc02e8717c01554fb", + "reference": "e0f87ad0e45f3898892a8f3bc02e8717c01554fb", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.1.* || 9.5.*" + }, + "type": "library", + "autoload": { + "classmap": [ + "geoPHP.inc" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Italo Mairo", + "homepage": "https://www.linkedin.com/in/italomairo/", + "role": "Maintanier of this Library Repo" + }, + { + "name": "Patrick Hayes", + "homepage": "https://www.linkedin.com/in/patrickdhayes/", + "role": "Maintanier of original Repositary/Library (https://github.com/phayes/geoPHP)" + } + ], + "description": "GeoPHP is a open-source native PHP library for doing geometry operations. It is written entirely in PHP and can therefore run on shared hosts. It can read and write a wide variety of formats: WKT (including EWKT), WKB (including EWKB), GeoJSON, KML, GPX, GeoRSS). It works with all Simple-Feature geometries (Point, LineString, Polygon, GeometryCollection etc.) and can be used to get centroids, bounding-boxes, area, and a wide variety of other useful information.", + "homepage": "https://github.com/itamair/geoPHP", + "support": { + "issues": "https://github.com/itamair/geoPHP/issues", + "source": "https://github.com/itamair/geoPHP/tree/1.12" + }, + "time": "2026-04-02T10:02:52+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + }, + "time": "2026-07-08T07:01:06+00:00" + }, + { + "name": "proj4php/proj4php", + "version": "v2.0.19", + "source": { + "type": "git", + "url": "https://github.com/proj4php/proj4php.git", + "reference": "52436c32e43efea3251d0c0d20f5fe387de46f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/proj4php/proj4php/zipball/52436c32e43efea3251d0c0d20f5fe387de46f6a", + "reference": "52436c32e43efea3251d0c0d20f5fe387de46f6a", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "9.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "proj4php\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "julien2512", + "email": "moquet.julien@gmail.com", + "homepage": "https://github.com/julien2512", + "role": "Developer" + }, + { + "name": "Jason Judge", + "email": "jason.judge@academe.co.uk", + "homepage": "http://academe.co.uk", + "role": "Developer" + }, + { + "name": "Nick Blackwell", + "email": "nickblackwell82@gmail.com", + "homepage": "https://people.ok.ubc.ca/nblackwe", + "role": "Developer" + } + ], + "description": "A PHP-Class for geographic coordinates transformation using proj4 definitions, thanks to a translation from Proj4JS", + "homepage": "https://github.com/proj4php/proj4php", + "keywords": [ + "coordinates", + "geographic", + "proj4", + "proj4js" + ], + "support": { + "issues": "https://github.com/proj4php/proj4php/issues", + "source": "https://github.com/proj4php/proj4php/tree/v2.0.19" + }, + "time": "2025-09-11T16:36:16+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -208,6 +667,157 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "symfony/asset", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "4bd4d143b7e53f40d45877df52eb2b18282bdac4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/4bd4d143b7e53f40d45877df52eb2b18282bdac4", + "reference": "4bd4d143b7e53f40d45877df52eb2b18282bdac4", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/http-client": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/asset-mapper", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset-mapper.git", + "reference": "b13fd8210675bafac7e782569d0cafac860f2e8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/b13fd8210675bafac7e782569d0cafac860f2e8f", + "reference": "b13fd8210675bafac7e782569d0cafac860f2e8f", + "shasum": "" + }, + "require": { + "composer/semver": "^3.0", + "php": ">=8.4.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0" + }, + "require-dev": { + "symfony/asset": "^7.4|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/event-dispatcher-contracts": "^3.0", + "symfony/finder": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/web-link": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\AssetMapper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps directories of assets & makes them available in a public directory with versioned filenames.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset-mapper/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-14T13:31:31+00:00" + }, { "name": "symfony/cache", "version": "v8.1.5", @@ -1395,42 +2005,55 @@ "time": "2026-08-21T19:39:55+00:00" }, { - "name": "symfony/http-foundation", + "name": "symfony/http-client", "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4" + "url": "https://github.com/symfony/http-client.git", + "reference": "9f941ed000bb11f16dc7eafed98a7b646d3b3e5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee16f97e95cfa011a742714d7c8c8f70fe7423f4", - "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4", + "url": "https://api.github.com/repos/symfony/http-client/zipball/9f941ed000bb11f16dc7eafed98a7b646d3b3e5d", + "reference": "9f941ed000bb11f16dc7eafed98a7b646d3b3e5d", "shasum": "" }, "require": { "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "^1.1" + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/http-client-contracts": "^3.7", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "doctrine/dbal": "<4.3" + "amphp/amp": "<3", + "php-http/discovery": "<1.15" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" }, "require-dev": { - "doctrine/dbal": "^4.3", - "predis/predis": "^1.1|^2.0", + "amphp/http-client": "^5.3.2", + "amphp/http-tunnel": "^2.0", + "guzzlehttp/guzzle": "^7.10|^8.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", "symfony/cache": "^7.4|^8.0", - "symfony/clock": "^7.4|^8.0", "symfony/dependency-injection": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", "symfony/http-kernel": "^7.4|^8.0", - "symfony/mime": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0" + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" + "Symfony\\Component\\HttpClient\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -1442,18 +2065,21 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Defines an object-oriented layer for the HTTP specification", + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", + "keywords": [ + "http" + ], "support": { - "source": "https://github.com/symfony/http-foundation/tree/v8.1.5" + "source": "https://github.com/symfony/http-client/tree/v8.1.5" }, "funding": [ { @@ -1473,77 +2099,41 @@ "type": "tidelift" } ], - "time": "2026-08-20T09:59:12+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/http-kernel", - "version": "v8.1.5", + "name": "symfony/http-client-contracts", + "version": "v3.7.1", "source": { "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6" + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/0306e1e65b90023fe40de6c6be95d06396bcb2e6", - "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41fc42d276aeff21192465331ebbab7d83a743c0", + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0", "shasum": "" }, "require": { - "php": ">=8.4.1", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^7.4|^8.0", - "symfony/event-dispatcher": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/dependency-injection": "<8.1", - "symfony/flex": "<2.10", - "symfony/http-client-contracts": "<2.5", - "symfony/serializer": "<7.4.15|>=8.0,<8.0.15|>=8.1,<8.1.2", - "symfony/translation-contracts": "<2.5", - "symfony/var-dumper": "<8.1", - "symfony/web-profiler-bundle": "<8.1", - "twig/twig": "<3.21" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" - }, - "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^7.4|^8.0", - "symfony/clock": "^7.4|^8.0", - "symfony/config": "^7.4|^8.0", - "symfony/console": "^7.4|^8.0", - "symfony/css-selector": "^7.4|^8.0", - "symfony/dependency-injection": "^8.1", - "symfony/dom-crawler": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/finder": "^7.4|^8.0", - "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^7.4|^8.0", - "symfony/property-access": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0", - "symfony/routing": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0", - "symfony/translation": "^7.4|^8.0", - "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^7.4|^8.0", - "symfony/validator": "^7.4|^8.0", - "symfony/var-dumper": "^8.1", - "symfony/var-exporter": "^7.4|^8.0", - "twig/twig": "^3.21|^4.0" + "php": ">=8.1" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" + "Symfony\\Contracts\\HttpClient\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1552,18 +2142,26 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a structured process for converting a Request into a Response", + "description": "Generic abstractions related to HTTP clients", "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "source": "https://github.com/symfony/http-kernel/tree/v8.1.5" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.1" }, "funding": [ { @@ -1583,20 +2181,211 @@ "type": "tidelift" } ], - "time": "2026-08-22T13:45:00+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { - "name": "symfony/polyfill-deepclone", - "version": "v1.42.0", + "name": "symfony/http-foundation", + "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-deepclone.git", - "reference": "70ba0627efc68e97ea392843458a2dd9d6dbd156" + "url": "https://github.com/symfony/http-foundation.git", + "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/70ba0627efc68e97ea392843458a2dd9d6dbd156", - "reference": "70ba0627efc68e97ea392843458a2dd9d6dbd156", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee16f97e95cfa011a742714d7c8c8f70fe7423f4", + "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-20T09:59:12+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/0306e1e65b90023fe40de6c6be95d06396bcb2e6", + "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/serializer": "<7.4.15|>=8.0,<8.0.15|>=8.1,<8.1.2", + "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-22T13:45:00+00:00" + }, + { + "name": "symfony/polyfill-deepclone", + "version": "v1.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-deepclone.git", + "reference": "70ba0627efc68e97ea392843458a2dd9d6dbd156" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/70ba0627efc68e97ea392843458a2dd9d6dbd156", + "reference": "70ba0627efc68e97ea392843458a2dd9d6dbd156", "shasum": "" }, "require": { @@ -1666,217 +2455,5558 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T06:33:24+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T08:25:59+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T06:33:24+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-01T12:47:55+00:00" + }, + { + "name": "symfony/property-access", + "version": "v8.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "1a41232c678972b93ce499a504e19ea09dfcd0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/1a41232c678972b93ce499a504e19ea09dfcd0b2", + "reference": "1a41232c678972b93ce499a504e19ea09dfcd0b2", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/property-info": "^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v8.1.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-30T12:40:56+00:00" + }, + { + "name": "symfony/property-info", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "b335f8e7fb1440ed3448fb33340efc6127678e53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/b335f8e7fb1440ed3448fb33340efc6127678e53", + "reference": "b335f8e7fb1440ed3448fb33340efc6127678e53", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/string": "^7.4|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3c188091b6b4fa2e4bc83a135caede12deb8576c", + "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-17T13:18:34+00:00" + }, + { + "name": "symfony/runtime", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/b7ea1abe04561e814b3134db0f56c287cedb35cc", + "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.4.1" + }, + "conflict": { + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "composer/composer": "^2.6", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dotenv": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Enables decoupling PHP applications from global state", + "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], + "support": { + "source": "https://github.com/symfony/runtime/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/serializer", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "9c064d505c661e3e17aa8999870f4fb0ed05e024" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/9c064d505c661e3e17aa8999870f4fb0ed05e024", + "reference": "9c064d505c661e3e17aa8999870f4fb0ed05e024", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/property-access": "<8.1", + "symfony/property-info": "<7.4.15", + "symfony/type-info": "<7.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/property-access": "^8.1", + "symfony/property-info": "^7.4.15|~8.0.15|^8.1.2", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-22T09:06:25+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T09:55:08+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T07:35:25+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "c721b1c98de3125ca195e35d52aaec5c97aa9c5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/c721b1c98de3125ca195e35d52aaec5c97aa9c5f", + "reference": "c721b1c98de3125ca195e35d52aaec5c97aa9c5f", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^3.25|^4.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/form": "<7.4.4|>8.0,<8.0.4", + "symfony/mime": "<7.4.9|>8.0,<8.0.9" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/asset": "^7.4|^8.0", + "symfony/asset-mapper": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/form": "^7.4.4|^8.0.4", + "symfony/html-sanitizer": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/mime": "^7.4.9|^8.0.9", + "symfony/polyfill-intl-icu": "^1.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/security-csrf": "^7.4|^8.0", + "symfony/security-http": "^7.4|^8.0", + "symfony/serializer": "^7.4.17|^8.1.5", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^7.4|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0", + "twig/cssinliner-extra": "^3", + "twig/inky-extra": "^3", + "twig/markdown-extra": "^3" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-22T09:06:25+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "a70b67c2cd990d05e544ea5fab361aa0e69fab3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/a70b67c2cd990d05e544ea5fab361aa0e69fab3a", + "reference": "a70b67c2cd990d05e544ea5fab361aa0e69fab3a", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.4.1", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" + }, + "require-dev": { + "symfony/asset": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/web-link": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/type-info", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "ceb48db5b38d6a48640c414be0c69d53980ae5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/ceb48db5b38d6a48640c414be0c69d53980ae5c5", + "reference": "ceb48db5b38d6a48640c414be0c69d53980ae5c5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "3783365b58972f4779254d98372af80fbf15e170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/3783365b58972f4779254d98372af80fbf15e170", + "reference": "3783365b58972f4779254d98372af80fbf15e170", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T20:10:55+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", + "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-deepclone": "^1.40" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to export, instantiate, hydrate, clone and lazy-load PHP objects", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "deep-clone", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-13T16:11:28+00:00" + }, + { + "name": "symfony/yaml", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", + "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:16:08+00:00" + }, + { + "name": "twig/extra-bundle", + "version": "v3.24.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/twig-extra-bundle.git", + "reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9", + "reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0|^8.0", + "twig/twig": "^3.2|^4.0" + }, + "require-dev": { + "league/commonmark": "^2.7", + "symfony/phpunit-bridge": "^6.4|^7.0", + "twig/cache-extra": "^3.0", + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Twig\\Extra\\TwigExtraBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Symfony bundle for extra Twig extensions", + "homepage": "https://twig.symfony.com", + "keywords": [ + "bundle", + "extra", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.24.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-02-07T08:07:38+00:00" + }, + { + "name": "twig/twig", + "version": "v3.28.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.28.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-07-03T20:44:34+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.1" + }, + "time": "2026-06-15T15:31:57+00:00" + } + ], + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "ergebnis/agent-detector", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-05-07T08:19:07+00:00" + }, + { + "name": "ergebnis/composer-normalize", + "version": "2.52.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/composer-normalize.git", + "reference": "988f83f5e51a42cdd2337e5fcd935432f8dfa33c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/988f83f5e51a42cdd2337e5fcd935432f8dfa33c", + "reference": "988f83f5e51a42cdd2337e5fcd935432f8dfa33c", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "ergebnis/json": "^1.4.0", + "ergebnis/json-normalizer": "^4.9.0", + "ergebnis/json-printer": "^3.7.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "localheinz/diff": "^1.3.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "composer/composer": "^2.9.8", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.62.1", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.11", + "phpunit/phpunit": "^9.6.33", + "rector/rector": "^2.4.3", + "symfony/filesystem": "^5.4.41" + }, + "type": "composer-plugin", + "extra": { + "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin", + "branch-alias": { + "dev-main": "2.52-dev" + }, + "plugin-optional": true, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Composer\\Normalize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a composer plugin for normalizing composer.json.", + "homepage": "https://github.com/ergebnis/composer-normalize", + "keywords": [ + "composer", + "normalize", + "normalizer", + "plugin" + ], + "support": { + "issues": "https://github.com/ergebnis/composer-normalize/issues", + "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/composer-normalize" + }, + "time": "2026-05-15T15:39:24+00:00" + }, + { + "name": "ergebnis/json", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json.git", + "reference": "7b56d2b5d9e897e75b43e2e753075a0904c921b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json/zipball/7b56d2b5d9e897e75b43e2e753075a0904c921b1", + "reference": "7b56d2b5d9e897e75b43e2e753075a0904c921b1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpstan-rules": "^2.11.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpstan/phpstan-strict-rules": "^2.0.6", + "phpunit/phpunit": "^9.6.24", + "rector/rector": "^2.1.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.7-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a Json value object for representing a valid JSON string.", + "homepage": "https://github.com/ergebnis/json", + "keywords": [ + "json" + ], + "support": { + "issues": "https://github.com/ergebnis/json/issues", + "security": "https://github.com/ergebnis/json/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json" + }, + "time": "2025-09-06T09:08:45+00:00" + }, + { + "name": "ergebnis/json-normalizer", + "version": "4.10.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-normalizer.git", + "reference": "77961faf2c651c3f05977b53c6c68e8434febf62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/77961faf2c651c3f05977b53c6c68e8434febf62", + "reference": "77961faf2c651c3f05977b53c6c68e8434febf62", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ergebnis/json-printer": "^3.5.0", + "ergebnis/json-schema-validator": "^4.2.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "composer/semver": "^3.4.3", + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.10", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.19", + "rector/rector": "^1.2.10" + }, + "suggest": { + "composer/semver": "If you want to use ComposerJsonNormalizer or VersionConstraintNormalizer" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.11-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Normalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.", + "homepage": "https://github.com/ergebnis/json-normalizer", + "keywords": [ + "json", + "normalizer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-normalizer/issues", + "security": "https://github.com/ergebnis/json-normalizer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-normalizer" + }, + "time": "2025-09-06T09:18:13+00:00" + }, + { + "name": "ergebnis/json-pointer", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-pointer.git", + "reference": "b58c3c468a7ff109fdf9a255f17de29ecbe5276c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/b58c3c468a7ff109fdf9a255f17de29ecbe5276c", + "reference": "b58c3c468a7ff109fdf9a255f17de29ecbe5276c", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.50.0", + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.16.0", + "fakerphp/faker": "^1.24.1", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.46", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.8-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Pointer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides an abstraction of a JSON pointer.", + "homepage": "https://github.com/ergebnis/json-pointer", + "keywords": [ + "RFC6901", + "json", + "pointer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-pointer/issues", + "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-pointer" + }, + "time": "2026-04-07T14:52:13+00:00" + }, + { + "name": "ergebnis/json-printer", + "version": "3.8.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-printer.git", + "reference": "211d73fc7ec6daf98568ee6ed6e6d133dee8503e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/211d73fc7ec6daf98568ee6ed6e6d133dee8503e", + "reference": "211d73fc7ec6daf98568ee6ed6e6d133dee8503e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.10", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.1", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.21", + "rector/rector": "^1.2.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.9-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Printer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON printer, allowing for flexible indentation.", + "homepage": "https://github.com/ergebnis/json-printer", + "keywords": [ + "formatter", + "json", + "printer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-printer/issues", + "security": "https://github.com/ergebnis/json-printer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-printer" + }, + "time": "2025-09-06T09:59:26+00:00" + }, + { + "name": "ergebnis/json-schema-validator", + "version": "4.5.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-schema-validator.git", + "reference": "b739527a480a9e3651360ad351ea77e7e9019df2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-schema-validator/zipball/b739527a480a9e3651360ad351ea77e7e9019df2", + "reference": "b739527a480a9e3651360ad351ea77e7e9019df2", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.10", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.20", + "rector/rector": "^1.2.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.6-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\SchemaValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON schema validator, building on top of justinrainbow/json-schema.", + "homepage": "https://github.com/ergebnis/json-schema-validator", + "keywords": [ + "json", + "schema", + "validator" + ], + "support": { + "issues": "https://github.com/ergebnis/json-schema-validator/issues", + "security": "https://github.com/ergebnis/json-schema-validator/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-schema-validator" + }, + "time": "2025-09-06T11:37:35+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.95.23", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "b2932a5af3157a0c28324b1efe5e3b556dee09c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b2932a5af3157a0c28324b1efe5e3b556dee09c4", + "reference": "b2932a5af3157a0c28324b1efe5e3b556dee09c4", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.2", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0 || ^9.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.37", + "symfony/polyfill-php80": "^1.37", + "symfony/polyfill-php81": "^1.37", + "symfony/polyfill-php84": "^1.37", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.10.0", + "keradus/cli-executor": "^2.3", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.35 || ^10.5.64 || ^11.5.56 || ^12.5.31 || ^13.0.6", + "symfony/polyfill-php85": "^1.38", + "symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.1", + "symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.1" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.23" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-08-26T19:54:54+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.11.0", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/7e420a943a6fbc95e60e3cf67acfbee85b3b4da7", + "reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "dev-main", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.11.0" + }, + "time": "2026-08-21T10:30:42+00:00" + }, + { + "name": "localheinz/diff", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/localheinz/diff.git", + "reference": "33bd840935970cda6691c23fc7d94ae764c0734c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/diff/zipball/33bd840935970cda6691c23fc7d94ae764c0734c", + "reference": "33bd840935970cda6691c23fc7d94ae764c0734c", + "shasum": "" + }, + "require": { + "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.0 || ^8.5.23", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize", + "homepage": "https://github.com/localheinz/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/localheinz/diff/issues", + "source": "https://github.com/localheinz/diff/tree/1.3.0" + }, + "time": "2025-08-30T09:44:18+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.10", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/36d1509c998b0602811824143526b4a9ee2774e7", + "reference": "36d1509c998b0602811824143526b4a9ee2774e7", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-08-30T12:46:16+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "14.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "6ce313bb110384148d1dc7695a99175f59529069" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6ce313bb110384148d1dc7695a99175f59529069", + "reference": "6ce313bb110384148d1dc7695a99175f59529069", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.8.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.2", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-08-16T05:23:47+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/9bb4e6c58b62c1e043be995c66abec7c97307aae", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:47:43+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:36:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "13.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "22104a5ceb8d642e6b30ab00211d53d88e2db368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/22104a5ceb8d642e6b30ab00211d53d88e2db368", + "reference": "22104a5ceb8d642e6b30ab00211d53d88e2db368", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.3.1", + "phpunit/php-file-iterator": "^7.0.2", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.1", + "sebastian/comparator": "^8.4", + "sebastian/diff": "^9.0", + "sebastian/environment": "^9.3.2", + "sebastian/exporter": "^8.2.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.1.0", + "sebastian/recursion-context": "^8.0.1", + "sebastian/type": "^7.0.2", + "sebastian/version": "^7.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.3-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.3.2" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-08-27T08:40:49+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-12-23T15:25:20+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "rector/rector", + "version": "2.6.5", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "f8686605798c755456327a3597201c3b5be76294" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f8686605798c755456327a3597201c3b5be76294", + "reference": "f8686605798c755456327a3597201c3b5be76294", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.2.10" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.6.5" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-08-30T22:02:04+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-08-01T04:27:14+00:00" + }, + { + "name": "sebastian/comparator", + "version": "8.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.2" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:23:13+00:00" + }, + { + "name": "sebastian/complexity", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00" + }, + { + "name": "sebastian/diff", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2df6626c1baf31d5a88674882a3072f151b5a26", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:38:55+00:00" + }, + { + "name": "sebastian/environment", + "version": "9.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.1.11" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:41:38+00:00" + }, + { + "name": "sebastian/exporter", + "version": "8.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:22:06+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.1.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:11:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-07-09T08:42:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/511064ecde82bd747e2ba2fab3dda8d977b59576", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" + } + ], + "time": "2026-08-13T07:05:05+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f71bbcdc4f95456b4622810bec64eb06372e25b2", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" + } + ], + "time": "2026-08-13T06:34:36+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "8.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-03T05:58:12+00:00" + }, + { + "name": "sebastian/type", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "bd1df467864cb95140414059a535b2d906173fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/bd1df467864cb95140414059a535b2d906173fcf", + "reference": "bd1df467864cb95140414059a535b2d906173fcf", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", "type": "tidelift" } ], - "time": "2026-08-07T06:33:24+00:00" + "time": "2026-08-10T08:00:57+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.41.0", + "name": "sebastian/version", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", - "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", "shasum": "" }, "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" + "php": ">=8.4" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "branch-alias": { + "dev-main": "7.0-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", "type": "tidelift" } ], - "time": "2026-07-28T08:25:59+00:00" + "time": "2026-02-06T04:52:52+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.42.0", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", - "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", "shasum": "" }, "require": { - "php": ">=7.2" + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "classmap": [ - "Resources/stubs" + "lib/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", + "description": "A static analysis tool to detect side effects in PHP code", "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" + "static analysis" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/staabm", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2026-08-07T06:33:24+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.38.2", + "name": "symfony/browser-kit", + "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + "url": "https://github.com/symfony/browser-kit.git", + "reference": "f6766c9fb232e72897d6d55cf8f6aec38db2dbbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f6766c9fb232e72897d6d55cf8f6aec38db2dbbf", + "reference": "f6766c9fb232e72897d6d55cf8f6aec38db2dbbf", "shasum": "" }, "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" + "php": ">=8.4.1", + "symfony/dom-crawler": "^7.4|^8.0" }, - "suggest": { - "ext-mbstring": "For best performance" + "require-dev": { + "symfony/css-selector": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1884,25 +8014,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + "source": "https://github.com/symfony/browser-kit/tree/v8.1.5" }, "funding": [ { @@ -1922,41 +8045,32 @@ "type": "tidelift" } ], - "time": "2026-05-27T06:59:30+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/polyfill-php85", - "version": "v1.41.0", + "name": "symfony/css-selector", + "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" + "url": "https://github.com/symfony/css-selector.git", + "reference": "a291fb5adb65f52a4bb315db2d803698315dc64d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", - "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/a291fb5adb65f52a4bb315db2d803698315dc64d", + "reference": "a291fb5adb65f52a4bb315db2d803698315dc64d", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4.1" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php85\\": "" + "Symfony\\Component\\CssSelector\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1965,24 +8079,22 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" + "source": "https://github.com/symfony/css-selector/tree/v8.1.5" }, "funding": [ { @@ -2002,38 +8114,39 @@ "type": "tidelift" } ], - "time": "2026-07-01T12:47:55+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/routing", - "version": "v8.1.5", + "name": "symfony/debug-bundle", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c" + "url": "https://github.com/symfony/debug-bundle.git", + "reference": "2da1f202b38f646dbee032529cfd8e727cd12cd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/3c188091b6b4fa2e4bc83a135caede12deb8576c", - "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/2da1f202b38f646dbee032529cfd8e727cd12cd1", + "reference": "2da1f202b38f646dbee032529cfd8e727cd12cd1", "shasum": "" }, "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "require-dev": { - "psr/log": "^1|^2|^3", "symfony/config": "^7.4|^8.0", "symfony/dependency-injection": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/yaml": "^7.4|^8.0" + "symfony/http-kernel": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, - "type": "library", + "require-dev": { + "symfony/web-profiler-bundle": "^7.4|^8.0" + }, + "type": "symfony-bundle", "autoload": { "psr-4": { - "Symfony\\Component\\Routing\\": "" + "Symfony\\Bundle\\DebugBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2053,16 +8166,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Maps an HTTP request to a set of configuration variables", + "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "homepage": "https://symfony.com", - "keywords": [ - "router", - "routing", - "uri", - "url" - ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.1.5" + "source": "https://github.com/symfony/debug-bundle/tree/v8.1.0" }, "funding": [ { @@ -2082,45 +8189,34 @@ "type": "tidelift" } ], - "time": "2026-08-17T13:18:34+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/runtime", - "version": "v8.1.0", + "name": "symfony/dom-crawler", + "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/runtime.git", - "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc" + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "94f70e1b2e5b7b8f8618871fd9eaad3e39e9f03d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/b7ea1abe04561e814b3134db0f56c287cedb35cc", - "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/94f70e1b2e5b7b8f8618871fd9eaad3e39e9f03d", + "reference": "94f70e1b2e5b7b8f8618871fd9eaad3e39e9f03d", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0|^2.0", - "php": ">=8.4.1" - }, - "conflict": { - "symfony/error-handler": "<7.4" - }, - "require-dev": { - "composer/composer": "^2.6", - "symfony/console": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/dotenv": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0" + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.0" }, - "type": "composer-plugin", - "extra": { - "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + "require-dev": { + "symfony/css-selector": "^7.4|^8.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Runtime\\": "", - "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + "Symfony\\Component\\DomCrawler\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2132,21 +8228,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Enables decoupling PHP applications from global state", + "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", - "keywords": [ - "runtime" - ], "support": { - "source": "https://github.com/symfony/runtime/tree/v8.1.0" + "source": "https://github.com/symfony/dom-crawler/tree/v8.1.5" }, "funding": [ { @@ -2166,74 +8259,86 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.7.1", + "name": "symfony/maker-bundle", + "version": "v1.67.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/6ce8b313845f16bcf385ee3cb31d8b24e30d5516", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516", "shasum": "" }, "require": { + "composer-runtime-api": "^2.1", + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "conflict": { - "ext-psr": "<1.1|>=2" + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" }, - "type": "library", + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.10|^3.0", + "doctrine/orm": "^2.15|^3", + "doctrine/persistence": "^3.1|^4.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "1.x-dev" } }, "autoload": { "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] + "Symfony\\Bundle\\MakerBundle\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.67.0" }, "funding": [ { @@ -2253,46 +8358,30 @@ "type": "tidelift" } ], - "time": "2026-06-16T09:55:08+00:00" + "time": "2026-03-18T13:39:06+00:00" }, { - "name": "symfony/string", - "version": "v8.1.2", + "name": "symfony/options-resolver", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + "url": "https://github.com/symfony/options-resolver.git", + "reference": "88f9c561f678a02d54b897014049fa839e33ff82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/88f9c561f678a02d54b897014049fa839e33ff82", + "reference": "88f9c561f678a02d54b897014049fa839e33ff82", "shasum": "" }, "require": { "php": ">=8.4.1", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-intl-grapheme": "^1.33", - "symfony/polyfill-intl-normalizer": "^1.0", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" - }, - "require-dev": { - "symfony/emoji": "^7.4|^8.0", - "symfony/http-client": "^7.4|^8.0", - "symfony/intl": "^7.4|^8.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { - "files": [ - "Resources/functions.php" - ], "psr-4": { - "Symfony\\Component\\String\\": "" + "Symfony\\Component\\OptionsResolver\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2304,26 +8393,23 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "description": "Provides an improved replacement for the array_replace PHP function", "homepage": "https://symfony.com", "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" + "config", + "configuration", + "options" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.2" + "source": "https://github.com/symfony/options-resolver/tree/v8.1.0" }, "funding": [ { @@ -2343,47 +8429,29 @@ "type": "tidelift" } ], - "time": "2026-07-28T07:35:25+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/var-dumper", + "name": "symfony/process", "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "61743d9bc7ab23b194527ca1be2fafd7dc93b74a" + "url": "https://github.com/symfony/process.git", + "reference": "d863f5e70d7c87abb906ac11b61f83036093000b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/61743d9bc7ab23b194527ca1be2fafd7dc93b74a", - "reference": "61743d9bc7ab23b194527ca1be2fafd7dc93b74a", + "url": "https://api.github.com/repos/symfony/process/zipball/d863f5e70d7c87abb906ac11b61f83036093000b", + "reference": "d863f5e70d7c87abb906ac11b61f83036093000b", "shasum": "" }, "require": { - "php": ">=8.4.1", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "symfony/console": "<7.4", - "symfony/error-handler": "<7.4" - }, - "require-dev": { - "symfony/console": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/uid": "^7.4|^8.0", - "twig/twig": "^3.12|^4.0" + "php": ">=8.4.1" }, - "bin": [ - "Resources/bin/var-dump-server" - ], "type": "library", "autoload": { - "files": [ - "Resources/functions/dump.php" - ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Component\\Process\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2395,22 +8463,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.1.5" + "source": "https://github.com/symfony/process/tree/v8.1.5" }, "funding": [ { @@ -2430,36 +8494,30 @@ "type": "tidelift" } ], - "time": "2026-08-21T12:16:08+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/var-exporter", - "version": "v8.1.5", + "name": "symfony/stopwatch", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/var-exporter.git", - "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1" + "url": "https://github.com/symfony/stopwatch.git", + "reference": "21c07b026905d596e8379caeb115d87aa479499d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", - "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/21c07b026905d596e8379caeb115d87aa479499d", + "reference": "21c07b026905d596e8379caeb115d87aa479499d", "shasum": "" }, "require": { "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-deepclone": "^1.40" - }, - "require-dev": { - "symfony/property-access": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\VarExporter\\": "" + "Symfony\\Component\\Stopwatch\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2471,29 +8529,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to export, instantiate, hydrate, clone and lazy-load PHP objects", + "description": "Provides a way to profile code", "homepage": "https://symfony.com", - "keywords": [ - "clone", - "construct", - "deep-clone", - "export", - "hydrate", - "instantiate", - "lazy-loading", - "proxy", - "serialize" - ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v8.1.5" + "source": "https://github.com/symfony/stopwatch/tree/v8.1.0" }, "funding": [ { @@ -2513,40 +8560,46 @@ "type": "tidelift" } ], - "time": "2026-08-13T16:11:28+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/yaml", + "name": "symfony/web-profiler-bundle", "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd" + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "4ae952afc65cb63fc74067633466bd8295cdcebc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", - "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4ae952afc65cb63fc74067633466bd8295cdcebc", + "reference": "4ae952afc65cb63fc74067633466bd8295cdcebc", "shasum": "" }, "require": { + "composer-runtime-api": ">=2.1", "php": ">=8.4.1", - "symfony/polyfill-ctype": "^1.8" + "symfony/config": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-kernel": "^8.1", + "symfony/routing": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0" }, "conflict": { - "symfony/console": "<7.4" + "symfony/serializer": "<7.4", + "symfony/workflow": "<7.4" }, "require-dev": { + "symfony/browser-kit": "^7.4|^8.0", "symfony/console": "^7.4|^8.0", - "yaml/yaml-test-suite": "*" + "symfony/css-selector": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", + "type": "symfony-bundle", "autoload": { "psr-4": { - "Symfony\\Component\\Yaml\\": "" + "Symfony\\Bundle\\WebProfilerBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2566,10 +8619,13 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Loads and dumps YAML files", + "description": "Provides a development tool that gives detailed information about the execution of any request", "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], "support": { - "source": "https://github.com/symfony/yaml/tree/v8.1.5" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v8.1.5" }, "funding": [ { @@ -2589,13 +8645,139 @@ "type": "tidelift" } ], - "time": "2026-08-21T12:16:08+00:00" + "time": "2026-08-22T09:06:25+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "vincentlanglet/twig-cs-fixer", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/VincentLanglet/Twig-CS-Fixer.git", + "reference": "73f6474720fce9a6709e7b6cdd26574efe36d09b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/73f6474720fce9a6709e7b6cdd26574efe36d09b", + "reference": "73f6474720fce9a6709e7b6cdd26574efe36d09b", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "ext-ctype": "*", + "php": ">=8.1", + "symfony/console": "^5.4.9 || ^6.4 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/string": "^5.4.42 || ^6.4.10 || ~7.0.10 || ^7.1.3 || ^8.0", + "twig/twig": "^3.15", + "webmozart/assert": "^1.10 || ^2.0" + }, + "require-dev": { + "composer/semver": "^3.2.0", + "dereuromark/composer-prefer-lowest": "^0.1.10", + "ergebnis/composer-normalize": "^2.29", + "friendsofphp/php-cs-fixer": "^3.13.0", + "infection/infection": "^0.26.16 || ^0.32.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + "phpunit/phpunit": "^9.5.26 || ^11.5.18 || ^12.1.3", + "rector/rector": "^2.0.0", + "shipmonk/composer-dependency-analyser": "^1.6", + "symfony/process": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/twig-bridge": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/ux-twig-component": "^2.2.0", + "twig/cache-extra": "^3.2" + }, + "bin": [ + "bin/twig-cs-fixer" + ], + "type": "coding-standard", + "autoload": { + "psr-4": { + "TwigCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Vincent Langlet" + } + ], + "description": "A tool to automatically fix Twig code style", + "homepage": "https://github.com/VincentLanglet/Twig-CS-Fixer", + "support": { + "issues": "https://github.com/VincentLanglet/Twig-CS-Fixer/issues", + "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/VincentLanglet", + "type": "github" + } + ], + "time": "2026-08-30T16:07:04+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -2603,6 +8785,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.9.0" } diff --git a/config/bundles.php b/config/bundles.php index 49d3fb6..93280e5 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -2,4 +2,9 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], ]; diff --git a/config/packages/asset_mapper.yaml b/config/packages/asset_mapper.yaml new file mode 100644 index 0000000..f7653e9 --- /dev/null +++ b/config/packages/asset_mapper.yaml @@ -0,0 +1,11 @@ +framework: + asset_mapper: + # The paths to make available to the asset mapper. + paths: + - assets/ + missing_import_mode: strict + +when@prod: + framework: + asset_mapper: + missing_import_mode: warn diff --git a/config/packages/debug.yaml b/config/packages/debug.yaml new file mode 100644 index 0000000..ad874af --- /dev/null +++ b/config/packages/debug.yaml @@ -0,0 +1,5 @@ +when@dev: + debug: + # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. + # See the "server:dump" command to start a new server. + dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 7e1ee1f..1e06583 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -8,6 +8,14 @@ framework: #esi: true #fragments: true + trusted_proxies: '127.0.0.1,REMOTE_ADDR' + + # https://symfony.com/doc/current/http_client.html#scoping-client + http_client: + scoped_clients: + broker.client: + base_uri: '%env(APP_BROKER_BASE_URI)%' + when@test: framework: test: true diff --git a/config/packages/property_info.yaml b/config/packages/property_info.yaml new file mode 100644 index 0000000..dd31b9d --- /dev/null +++ b/config/packages/property_info.yaml @@ -0,0 +1,3 @@ +framework: + property_info: + with_constructor_extractor: true diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml new file mode 100644 index 0000000..3f795d9 --- /dev/null +++ b/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + file_name_pattern: '*.twig' + +when@test: + twig: + strict_variables: true diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml new file mode 100644 index 0000000..456fc45 --- /dev/null +++ b/config/packages/web_profiler.yaml @@ -0,0 +1,11 @@ +when@dev: + web_profiler: + toolbar: true + + framework: + profiler: true + +when@test: + framework: + profiler: + collect: false diff --git a/config/reference.php b/config/reference.php index ececc72..1af6c7d 100644 --- a/config/reference.php +++ b/config/reference.php @@ -267,7 +267,7 @@ * formats?: array>, * }, * assets?: bool|array{ // Assets configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false * version_strategy?: scalar|Param|null, // Default: null * version?: scalar|Param|null, // Default: null @@ -286,7 +286,7 @@ * }>, * }, * asset_mapper?: bool|array{ // Asset Mapper configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * paths?: Param|string|array, * excluded_patterns?: list, * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true @@ -352,7 +352,7 @@ * }>, * }, * serializer?: bool|array{ // Serializer configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * enable_attributes?: bool|Param, // Default: true * name_converter?: scalar|Param|null, * circular_reference_handler?: scalar|Param|null, @@ -369,7 +369,7 @@ * }>, * }, * property_access?: bool|array{ // Property access configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * magic_call?: bool|Param, // Default: false * magic_get?: bool|Param, // Default: true * magic_set?: bool|Param, // Default: true @@ -377,11 +377,11 @@ * throw_exception_on_invalid_property_path?: bool|Param, // Default: true * }, * type_info?: bool|array{ // Type info configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * aliases?: array, * }, * property_info?: bool|array{ // Property info configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. // Default: true * }, * cache?: array{ // Cache configuration @@ -471,7 +471,7 @@ * }, * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true * http_client?: bool|array{ // HTTP Client configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * max_host_connections?: int|Param, // The maximum number of connections to a single host. * default_options?: array{ * headers?: array, @@ -701,28 +701,140 @@ * }, * }, * } + * @psalm-type MakerConfig = array{ + * root_namespace?: scalar|Param|null, // Default: "App" + * generate_final_classes?: bool|Param, // Default: true + * generate_final_entities?: bool|Param, // Default: false + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array + * }>, + * autoescape_service?: scalar|Param|null, // Default: null + * autoescape_service_method?: scalar|Param|null, // Default: null + * cache?: scalar|Param|null, // Default: true + * charset?: scalar|Param|null, // Default: "%kernel.charset%" + * debug?: bool|Param, // Default: "%kernel.debug%" + * strict_variables?: bool|Param, // Default: "%kernel.debug%" + * auto_reload?: scalar|Param|null, + * optimizations?: int|Param, + * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: Param|string|list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|Param|null, // Default: "F j, Y H:i" + * interval_format?: scalar|Param|null, // Default: "%d days" + * timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int|Param, // Default: 0 + * decimal_point?: scalar|Param|null, // Default: "." + * thousands_separator?: scalar|Param|null, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } + * @psalm-type TwigExtraConfig = array{ + * cache?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * html?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * markdown?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * intl?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * cssinliner?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * inky?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * string?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * commonmark?: array{ + * renderer?: array{ // Array of options for rendering HTML. + * block_separator?: scalar|Param|null, + * inner_separator?: scalar|Param|null, + * soft_break?: scalar|Param|null, + * }, + * html_input?: "strip"|"allow"|"escape"|Param, // How to handle HTML input. + * allow_unsafe_links?: bool|Param, // Remove risky link and image URLs by setting this to false. // Default: true + * max_nesting_level?: int|Param, // The maximum nesting level for blocks. // Default: 9223372036854775807 + * max_delimiters_per_line?: int|Param, // The maximum number of strong/emphasis delimiters per line. // Default: 9223372036854775807 + * slug_normalizer?: array{ // Array of options for configuring how URL-safe slugs are created. + * instance?: mixed, + * max_length?: int|Param, // Default: 255 + * unique?: mixed, + * }, + * commonmark?: array{ // Array of options for configuring the CommonMark core extension. + * enable_em?: bool|Param, // Default: true + * enable_strong?: bool|Param, // Default: true + * use_asterisk?: bool|Param, // Default: true + * use_underscore?: bool|Param, // Default: true + * unordered_list_markers?: list, + * }, + * ... + * }, + * } + * @psalm-type WebProfilerConfig = array{ + * toolbar?: bool|array{ // Profiler toolbar configuration + * enabled?: bool|Param, // Default: false + * ajax_replace?: bool|Param, // Replace toolbar on AJAX requests // Default: false + * }, + * intercept_redirects?: bool|Param, // Default: false + * excluded_ajax_paths?: scalar|Param|null, // Default: "^/((index|app(_[\\w]+)?)\\.php/)?_wdt" + * } + * @psalm-type DebugConfig = array{ + * max_items?: int|Param, // Max number of displayed items past the first level, -1 means no limit. // Default: 2500 + * min_depth?: int|Param, // Minimum tree depth to clone all the items, 1 is default. // Default: 1 + * max_string_length?: int|Param, // Max length of displayed strings, -1 means no limit. // Default: -1 + * dump_destination?: scalar|Param|null, // A stream URL where dumps should be written to. // Default: null + * theme?: "dark"|"light"|Param, // Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light". // Default: "dark" + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, * services?: ServicesConfig, * framework?: FrameworkConfig, + * twig?: TwigConfig, + * twig_extra?: TwigExtraConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, * services?: ServicesConfig, * framework?: FrameworkConfig, + * maker?: MakerConfig, + * twig?: TwigConfig, + * twig_extra?: TwigExtraConfig, + * web_profiler?: WebProfilerConfig, + * debug?: DebugConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, * services?: ServicesConfig, * framework?: FrameworkConfig, + * twig?: TwigConfig, + * twig_extra?: TwigExtraConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, * services?: ServicesConfig, * framework?: FrameworkConfig, + * twig?: TwigConfig, + * twig_extra?: TwigExtraConfig, + * web_profiler?: WebProfilerConfig, * }, * ... + */ +return [ + 'app' => ['path' => './assets/app.js', 'entrypoint' => true], + 'septima-widget' => ['version' => '3.23.0'], +]; diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..23c7172 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,20 @@ +parameters: + level: 6 + paths: + - bin/ + - config/ + - public/ + - src/ + # - tests/ + + excludePaths: + - src/Kernel.php + + ignoreErrors: + - message: '#Access to protected property proj4php\\Point::\$(x|y).#' + path: src/Command/BrokerImportGeoJson.php + + - messages: + - '#contains generic class RecursiveArrayIterator but does not specify its types#' + - '#Call to an undefined method RecursiveIterator::getArrayCopy\(\).#' + path: src/GeometryHelper.php diff --git a/phpunit.dist.xml b/phpunit.dist.xml new file mode 100644 index 0000000..22bd879 --- /dev/null +++ b/phpunit.dist.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + tests + + + + + + src + + + + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend + trigger_deprecation + + + + + + diff --git a/src/Command/BrokerEntityDelete.php b/src/Command/BrokerEntityDelete.php new file mode 100644 index 0000000..98bba64 --- /dev/null +++ b/src/Command/BrokerEntityDelete.php @@ -0,0 +1,96 @@ +request(Request::METHOD_GET, '/ngsi-ld/v1/types')->toArray(); + $entityTypes = $data['typeList'] ?? []; + } + + $limit = 1000; + foreach ($entityTypes as $entityType) { + while (true) { + /** + * @var array $entities + */ + $response = $brokerClient->request(Request::METHOD_GET, '/ngsi-ld/v1/entities', [ + 'query' => [ + 'type' => $entityType, + 'limit' => $limit, + ], + ]); + + $entities = $response->toArray(); + $count = count($entities); + if ($count > 0) { + $io->writeln(match ($count) { + 1 => sprintf('Deleting one %s entity …', $entityType), + default => sprintf('Deleting %d %s entities …', $count, $entityType), + }); + $io->progressStart($count); + foreach ($entities as $entity) { + $brokerClient->request(Request::METHOD_DELETE, '/ngsi-ld/v1/entities/' . urlencode($entity['id'])) + ->getStatusCode(); + $io->progressAdvance(); + } + $io->progressFinish(); + } + + if ($count < $limit) { + break; + } + } + } + + return Command::SUCCESS; + } + + /** + * Get link URL. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + */ + private function getLinkUrl(ResponseInterface $response, string $rel): ?string + { + $links = $response->getHeaders()['link'] ?? []; + foreach ($links as $link) { + if (preg_match_all('/<(?P[^>]+)>;\s*rel="(?P[^"]+)"/', (string)$link, $matches, PREG_SET_ORDER)) { + if ($rel === ($matches[0]['rel'] ?? null)) { + return $matches[0]['url'] ?? null; + } + } + } + + return null; + } +} diff --git a/src/Command/BrokerImportGeoJson.php b/src/Command/BrokerImportGeoJson.php new file mode 100644 index 0000000..b70ad4f --- /dev/null +++ b/src/Command/BrokerImportGeoJson.php @@ -0,0 +1,101 @@ +request(Request::METHOD_GET, $dataUrl)->toArray(); + $features = $data['features'] ?? null; + $sourceProjection = $data['crs']['properties']['name'] ?? null; + $sourceProjection = null !== $sourceProjection + ? Crs::from($sourceProjection) + : Crs::geoJson(); + + $count = count($features); + if ($count > 0) { + $io->writeln(match ($count) { + 1 => sprintf('Creating one %s entity …', $entityType), + default => sprintf('Creating %d %s entities …', $count, $entityType), + }); + $io->progressStart($count); + foreach ($features as $index => $feature) { + $entity = [ + 'type' => $entityType, + 'id' => sprintf('%s:%04d', $entityType, $index), + ]; +// foreach (['geometry'] as $key) { +// if (array_key_exists($key, $feature)) { +// $entity[self::RESERVED_PROPERTY_NAME_PREFIX.$key] = $feature[$key]; +// } +// } + foreach ($feature['properties'] as $name => $value) { + if (null === $value) { + continue; + } + if (in_array($name, ['id', 'type'])) { + $name = self::RESERVED_PROPERTY_NAME_PREFIX.$name; + } + $entity[$name] = [ + 'type' => 'Property', + 'value' => $value, + ]; + } + $geometry = $geometryHelper->transformGeoJsonGeometry($feature['geometry'], from: $sourceProjection); + $entity['geometry'] = $geometry; + $centroid = $geometryHelper->getCentroid($geometry); + // Compute location from geometry. The location must be a single point (the "spatial location" of the entity). + $entity['location'] = [ + 'type' => 'geo:json', + 'value' => [ + 'type' => 'Point', + 'coordinates' => [$centroid->x, $centroid->y], + ], + ]; + $response = $brokerClient->request(Request::METHOD_POST, '/ngsi-ld/v1/entities', [ + 'json' => $entity, + ]); + + if (Response::HTTP_CREATED !== $response->getStatusCode()) { + $io->error($response->getContent(false)); + $io->writeln(json_encode($entity, JSON_PRETTY_PRINT)); + + return Command::FAILURE; + } + $io->progressAdvance(); + } + $io->progressFinish(); + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/ConvertPointCommand.php b/src/Command/ConvertPointCommand.php new file mode 100644 index 0000000..4a1c3a0 --- /dev/null +++ b/src/Command/ConvertPointCommand.php @@ -0,0 +1,46 @@ +createProjection($from->value)); + $transformedPoint = $helper->transformPoint($point, to: $to); + + $io->writeln([ + $point->toShortString(), + $from->name, + $transformedPoint->toShortString(), + $to->name, + ]); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/DataController.php b/src/Controller/DataController.php new file mode 100644 index 0000000..38766ad --- /dev/null +++ b/src/Controller/DataController.php @@ -0,0 +1,65 @@ + '[^.]+', + '_format' => 'json|geojson', + ], + defaults: ['_format' => self::FORMAT_JSON], + )] + public function index(Request $request, string $path, string $_format, + HttpClientInterface $brokerClient, + ): JsonResponse { + $path = '/'.ltrim($path, '/'); + $headers = $request->headers->all(); + // Set "accept" header for clients that cannot do it themselves. + if (self::FORMAT_GEOJSON === $_format) { + $headers['accept'] = self::APPLICATION_GEOJSON; + } + // Exclude some headers from the proxy call to the broker. + $headers = array_filter( + $headers, + static fn (string $name) => !in_array($name, ['authorization', 'host']) + && !str_starts_with($name, 'x-forwarded-'), + ARRAY_FILTER_USE_KEY + ); + $response = $brokerClient->request($request->getMethod(), $path, [ + 'query' => $request->query->all(), + 'headers' => $headers, + ]); + + return new JsonResponse( + data: $response->getContent(), + status: $response->getStatusCode(), + headers: $response->getHeaders(), + json: true, + ); + } + + #[Route('/test', name: 'data_test')] + public function test(): JsonResponse + { + $data = Yaml::parseFile(__DIR__.'/data.yaml'); + + return new JsonResponse($data); + } +} diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php new file mode 100644 index 0000000..674b131 --- /dev/null +++ b/src/Controller/DefaultController.php @@ -0,0 +1,18 @@ + Requirement::CATCH_ALL], methods: [Request::METHOD_GET])] + public function index(?string $path = null): Response + { + return $this->render(null === $path ? 'default/index.html.twig' : sprintf('default/%s.html.twig', $path)); + } +} diff --git a/src/GeometryHelper.php b/src/GeometryHelper.php new file mode 100644 index 0000000..05006c4 --- /dev/null +++ b/src/GeometryHelper.php @@ -0,0 +1,85 @@ + + */ +final class GeometryHelper +{ + /** + * @return array{ + * 0: float, + * 1: float, + * } + */ + public function transformCoordinates(float $x, float $y, Crs|string $from, string|Crs|null $to = Crs::EPSG_4326): array + { + $point = new Point($x, $y); + $transformed = $this->transformPoint($point, $to, $from); + + return array_slice($transformed->toArray(), 0, 2); + } + + public function transformPoint(Point $point, Crs|string|null $from = null, Crs|string $to = Crs::EPSG_4326): Point + { + if (null === $point->getProjection() && null === $from) { + throw new \LogicException('Projection must be specified when point has no projection'); + } + + $proj4 = new Proj4php(); + $fromProj = $point->getProjection() ?? new Proj(is_string($from) ? $from : $from->value, $proj4); + $toProj = new Proj(is_string($to) ? $to : $to->value, $proj4); + + // Proj4php::transform modifies its argument! + return $proj4->transform($fromProj, $toProj, clone $point); + } + + public function createProjection(string $srsCode): Proj + { + $proj4 = new Proj4php(); + + return new Proj($srsCode, $proj4); + } + + /** + * @param Geometry $geometry + * + * @return Geometry + */ + public function transformGeoJsonGeometry(array $geometry, Crs|string|null $from, Crs|string $to = Crs::EPSG_4326): array + { + $arrayIterator = new \RecursiveArrayIterator($geometry); + $iterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::CHILD_FIRST); + foreach ($iterator as $key => $value) { + if (is_array($value) && array_is_list($value) && 2 === count($value) && is_float($value[0]) && is_float($value[1])) { + $transformed = $this->transformCoordinates($value[0], $value[1], $to, $from); + // Am I stupid or is PHP stupid? + // https://stackoverflow.com/a/40483953 + $currentDepth = $iterator->getDepth(); + for ($subDepth = $currentDepth; $subDepth >= 0; --$subDepth) { + /** @var \RecursiveArrayIterator $subIterator */ + $subIterator = $iterator->getSubIterator($subDepth); + $subIterator->offsetSet($subIterator->key(), $subDepth === $currentDepth ? $transformed : $iterator->getSubIterator($subDepth + 1)->getArrayCopy()); + } + } + } + + return $arrayIterator->getArrayCopy(); + } + + /** + * @param Geometry $geometry + */ + public function getCentroid(array $geometry): Point + { + $centroid = \geoPHP::load(json_encode($geometry), 'geojson')->getCentroid(); + + return new Point($centroid->getX(), $centroid->getY()); + } +} diff --git a/src/GeometryHelper/Crs.php b/src/GeometryHelper/Crs.php new file mode 100644 index 0000000..8c67349 --- /dev/null +++ b/src/GeometryHelper/Crs.php @@ -0,0 +1,15 @@ + + + + + + {% block title %}Enter{% endblock %} + {% block stylesheets %} + + {% endblock %} + + {% block javascripts %} + + {% block importmap %}{{ importmap('app') }}{% endblock %} + {% endblock %} + + + {% block body %} +
+

{{ block('title') }}

+ + {% block flashes %} + {% for type, messages in app.flashes %} + {% for message in messages %} + + {% endfor %} + {% endfor %} + {% endblock %} + + {% block content %}{% endblock %} +
+{% endblock %} + + diff --git a/templates/default/hundeskov.html.twig b/templates/default/hundeskov.html.twig new file mode 100644 index 0000000..611eb6b --- /dev/null +++ b/templates/default/hundeskov.html.twig @@ -0,0 +1,113 @@ +{% extends 'base.html.twig' %} + +{% block content %} +
+ + + +{# +
+ + +#} +{% endblock %} diff --git a/templates/default/index.html.twig b/templates/default/index.html.twig new file mode 100644 index 0000000..af084f0 --- /dev/null +++ b/templates/default/index.html.twig @@ -0,0 +1,63 @@ +{% extends 'base.html.twig' %} + +{% block content %} + {% for path in ['hundeskov'] %} + {{ path }} + {% endfor %} + + +
#map
+ + + +{# +4.9 NGSI-LD Query Language: https://cim.etsi.org/NGSI-LD/official/clause-4.html#4.9 +4.10 NGSI-LD Geoquery Language: https://cim.etsi.org/NGSI-LD/official/clause-4.html#4.10 +#} +
    +{% for query in [ +{type: 'handicapparkering'}, +{type: 'hundeskov'}, +{type: 'toilet'}, +{q: 'beskrivelse==Handicaptoilet'}, + +{ +georel: 'near;maxDistance==20000', +geometry: 'Point', +coordinates: [10, 56]|json_encode, +}, + +{ +georel: 'near;maxDistance==2000', +geometry: 'Point', +coordinates: [10.236808047161853, 56.101226991176155]|json_encode, +}, + +{ +georel: 'near;maxDistance==8000', +geometry: 'Point', +coordinates: [10.236808047161853, 56.101226991176155]|json_encode, +}, + +] %} +{% set url = path('app_data', {path: 'ngsi-ld/v1/entities', _format: 'geojson'}|merge(query)) %} + +{# +Scorpio complains about "Bad Request Data." and says "Georelation is not valid" when `==` is URL encoded! +#} +{% set url = url|replace({('maxDistance=='|url_encode): 'maxDistance=='}) %} + +
  • {{ url }}
  • +{% endfor %} +
+ +Reset +
+{##} +{# https://github.com/Septima/widget-issues/issues/24#issuecomment-5478400091 #} + + +https://enter.ddev.site:33001/data/ngsi-ld/v1/entities.geojson?georel=near;maxDistance==2000&geometry=Point&coordinates=[8,40] + + +{% endblock %} diff --git a/tests/GeometryHelperTest.php b/tests/GeometryHelperTest.php new file mode 100644 index 0000000..000e9dc --- /dev/null +++ b/tests/GeometryHelperTest.php @@ -0,0 +1,61 @@ +transformCoordinates($input[0], $input[1], GeometryHelper::EPSG_GEO_JSON, GeometryHelper::EPSG_25832); + $this->assertEquals($expected, $actual); + } + + public function testTransformPoint(): void + { + $helper = new GeometryHelper(); + $input = new Point( + 576933.0181395365, 6218035.22691216, + ); + $expected = new Point( + 10.236808, 56.101227, + ); + $actual = $helper->transformPoint($input, GeometryHelper::EPSG_GEO_JSON, GeometryHelper::EPSG_25832); + $this->assertEquals($expected, $actual); + } + + public function testTransformGeoJsonGeometry(): void + { + $helper = new GeometryHelper(); + + $input = [ + 'type' => 'MultiPoint', + 'coordinates' => [ + [ + 576933.0181395365, 6218035.22691216, + ], + ], + ]; + $expected = [ + 'type' => 'MultiPoint', + 'coordinates' => [ + [ + // https://epsg.io/transform#s_srs=25832&t_srs=4326&x=576933.0181395&y=6218035.2269122 + 10.236808, 56.101227, + ], + ], + ]; + $actual = $helper->transformGeoJsonGeometry($input, from: GeometryHelper::EPSG_25832, to: GeometryHelper::EPSG_GEO_JSON); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..47a5855 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,13 @@ +bootEnv(dirname(__DIR__).'/.env'); +} + +if ($_SERVER['APP_DEBUG']) { + umask(0000); +} From 190ace828e7cbe43e4788ae87f654e3fead06bd1 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 1 Sep 2026 10:57:52 +0200 Subject: [PATCH 004/101] Added ITKDev project template --- .ddev/docker-compose.scorpio.yaml | 2 +- .docker/data/.gitignore | 5 + .docker/data/README.md | 26 +++ .docker/nginx.conf | 34 ++++ .docker/templates/default.conf.template | 56 +++++++ .env | 4 + .github/workflows/changelog.yaml | 27 +++ .github/workflows/composer.yaml | 79 +++++++++ .github/workflows/javascript.yaml | 39 +++++ .github/workflows/markdown.yaml | 45 +++++ .github/workflows/php.yaml | 66 ++++++++ .github/workflows/styles.yaml | 40 +++++ .github/workflows/twig.yaml | 55 ++++++ .github/workflows/yaml.yaml | 45 +++++ .markdownlint.jsonc | 22 +++ .markdownlintignore | 12 ++ .prettierignore | 2 + .prettierrc.yaml | 18 ++ CHANGELOG.md | 10 ++ README.md | 14 +- Taskfile.yml | 42 ++--- assets/app.js | 4 +- composer.json | 28 ++-- composer.lock | 214 ++++++++++++------------ config/packages/cache.yaml | 2 +- config/packages/debug.yaml | 2 +- config/services.yaml | 2 +- docker-compose.dev.yml | 27 +++ docker-compose.redirect.yml | 15 ++ docker-compose.server.yml | 52 ++++++ docker-compose.yml | 110 ++++++++++++ phpstan.dist.neon | 4 +- rector.php | 18 ++ src/Command/BrokerEntityDelete.php | 13 +- src/Command/BrokerImportGeoJson.php | 8 - src/Controller/DataController.php | 4 +- src/GeometryHelper.php | 2 +- templates/default/hundeskov.html.twig | 4 +- templates/default/index.html.twig | 8 +- tests/{ => Unit}/GeometryHelperTest.php | 8 +- tests/bootstrap.php | 2 +- 41 files changed, 986 insertions(+), 184 deletions(-) create mode 100644 .docker/data/.gitignore create mode 100644 .docker/data/README.md create mode 100644 .docker/nginx.conf create mode 100644 .docker/templates/default.conf.template create mode 100644 .github/workflows/changelog.yaml create mode 100644 .github/workflows/composer.yaml create mode 100644 .github/workflows/javascript.yaml create mode 100644 .github/workflows/markdown.yaml create mode 100644 .github/workflows/php.yaml create mode 100644 .github/workflows/styles.yaml create mode 100644 .github/workflows/twig.yaml create mode 100644 .github/workflows/yaml.yaml create mode 100644 .markdownlint.jsonc create mode 100644 .markdownlintignore create mode 100644 .prettierignore create mode 100644 .prettierrc.yaml create mode 100644 CHANGELOG.md create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.redirect.yml create mode 100644 docker-compose.server.yml create mode 100644 docker-compose.yml create mode 100644 rector.php rename tests/{ => Unit}/GeometryHelperTest.php (88%) diff --git a/.ddev/docker-compose.scorpio.yaml b/.ddev/docker-compose.scorpio.yaml index e1f60a2..a8a12ec 100644 --- a/.ddev/docker-compose.scorpio.yaml +++ b/.ddev/docker-compose.scorpio.yaml @@ -1,5 +1,5 @@ networks: - scorpio: + scorpio: # https://docs.ddev.com/en/stable/users/extend/custom-docker-services/ services: diff --git a/.docker/data/.gitignore b/.docker/data/.gitignore new file mode 100644 index 0000000..4ce1020 --- /dev/null +++ b/.docker/data/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except +!.gitignore +!README.md diff --git a/.docker/data/README.md b/.docker/data/README.md new file mode 100644 index 0000000..8895d7b --- /dev/null +++ b/.docker/data/README.md @@ -0,0 +1,26 @@ +# .docker/data + +Please map persistent volumes to this directory on the servers. + +If a container needs to persist data between restarts you can map the relevant files in the container to ``docker/data/`. + +## RabbitMQ example +If you are using RabbitMQ running in a container as a message broker you need to configure a persistent volume for RabbitMQs data directory to avoid losing message on container restarts. + +```yaml +# docker-compose.server.override.yml + +services: + rabbit: + image: rabbitmq:3.9-management-alpine + hostname: "${COMPOSE_PROJECT_NAME}" + networks: + - app + - frontend + environment: + - "RABBITMQ_DEFAULT_USER=${RABBITMQ_USER}" + - "RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD}" + - "RABBITMQ_ERLANG_COOKIE=${RABBITMQ_ERLANG_COOKIE}" + volumes: + - ".docker/data/rabbitmq:/var/lib/rabbitmq/mnesia/" +``` diff --git a/.docker/nginx.conf b/.docker/nginx.conf new file mode 100644 index 0000000..ec278a5 --- /dev/null +++ b/.docker/nginx.conf @@ -0,0 +1,34 @@ +worker_processes auto; + +error_log /dev/stderr notice; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + proxy_temp_path /tmp/proxy_temp; + client_body_temp_path /tmp/client_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Note: set_real_ip_from is set in the server block + + log_format main '$http_x_real_ip - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/stdout main; + + sendfile on; + keepalive_timeout 65; + + gzip on; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/.docker/templates/default.conf.template b/.docker/templates/default.conf.template new file mode 100644 index 0000000..36ddf04 --- /dev/null +++ b/.docker/templates/default.conf.template @@ -0,0 +1,56 @@ +server { + listen ${NGINX_PORT}; + server_name localhost; + + root ${NGINX_WEB_ROOT}; + + client_max_body_size ${NGINX_MAX_BODY_SIZE}; + + set_real_ip_from 172.16.0.0/16; + set_real_ip_from 192.168.39.0/24; + real_ip_recursive on; + real_ip_header X-Forwarded-For; + + location = /cron-metrics { + # Proxy to supercronic metrics + proxy_pass http://${NGINX_CRON_METRICS}/metrics; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + # try to serve file directly, fallback to index.php + try_files $uri /index.php$is_args$args; + } + + # Protect files and directories from prying eyes. + location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|.tar|.gz|.bz2|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { + deny all; + return 404; + } + + location ~ ^/index\.php(/|$) { + fastcgi_buffers 16 32k; + fastcgi_buffer_size 64k; + fastcgi_busy_buffers_size 64k; + + fastcgi_pass ${NGINX_FPM_SERVICE}; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + + internal; + } + + location ~ \.php$ { + return 404; + } + + # Send log message to files symlinked to stdout/stderr. + error_log /dev/stderr; + access_log /dev/stdout main; +} diff --git a/.env b/.env index b9dbe2b..999fa0f 100644 --- a/.env +++ b/.env @@ -1,3 +1,7 @@ +COMPOSE_PROJECT_NAME=enter +COMPOSE_DOMAIN=enter.local.itkdev.dk +ITKDEV_TEMPLATE=symfony-8 + # In all environments, the following files are loaded if they exist, # the latter taking precedence over the former: # diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..09e309d --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,27 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/changelog.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Changelog +### +### Checks that changelog has been updated + +name: Changelog + +on: + pull_request: + +jobs: + changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 2 + + - name: Git fetch + run: git fetch + + - name: Check that changelog has been updated. + run: git diff --exit-code origin/${{ github.base_ref }} -- CHANGELOG.md && exit 1 || exit 0 diff --git a/.github/workflows/composer.yaml b/.github/workflows/composer.yaml new file mode 100644 index 0000000..fd10569 --- /dev/null +++ b/.github/workflows/composer.yaml @@ -0,0 +1,79 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/composer.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Composer +### +### Validates composer.json and checks that it's normalized. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [ergebnis/composer-normalize](https://github.com/ergebnis/composer-normalize) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev ergebnis/composer-normalize +### ``` +### +### Normalize `composer.json` by running +### +### ``` shell +### docker compose run --rm phpfpm composer normalize +### ``` + +name: Composer + +env: + COMPOSE_USER: runner + +on: + pull_request: + paths: &paths + - "composer.json" + - "composer.lock" + - "docker-compose.yml" + push: + branches: + - main + - develop + paths: *paths + +jobs: + composer-validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer validate --strict + + composer-normalized: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm composer normalize --dry-run + + composer-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer audit --locked diff --git a/.github/workflows/javascript.yaml b/.github/workflows/javascript.yaml new file mode 100644 index 0000000..7960aed --- /dev/null +++ b/.github/workflows/javascript.yaml @@ -0,0 +1,39 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/symfony/javascript.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Symfony JavaScript (and TypeScript) +### +### Validates JavaScript files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. + +name: JavaScript + +on: + pull_request: + paths: &paths + - "assets/**/*.js" + - "docker-compose.yml" + push: + branches: + - main + - develop + paths: *paths + +jobs: + javascript-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier 'assets/**/*.js' --check diff --git a/.github/workflows/markdown.yaml b/.github/workflows/markdown.yaml new file mode 100644 index 0000000..cee4ff8 --- /dev/null +++ b/.github/workflows/markdown.yaml @@ -0,0 +1,45 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/markdown.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Markdown +### +### Lints Markdown files (`**/*.md`) in the project. +### +### [markdownlint-cli configuration +### files](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration), +### `.markdownlint.jsonc` and `.markdownlintignore`, control what is actually +### linted and how. +### +### #### Assumptions +### +### 1. A docker compose service named `markdownlint` for running `markdownlint` +### (from +### [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli)) +### exists. + +name: Markdown + +on: + pull_request: + paths: &paths + - "**/*.md" + push: + branches: + - main + - develop + paths: *paths + +jobs: + markdown-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm markdownlint markdownlint '**/*.md' diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml new file mode 100644 index 0000000..b005255 --- /dev/null +++ b/.github/workflows/php.yaml @@ -0,0 +1,66 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/symfony/php.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Symfony PHP +### +### Checks that PHP code adheres to the [Symfony coding +### standards](https://symfony.com/doc/current/contributing/code/standards.html). +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. 2. +### [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev friendsofphp/php-cs-fixer +### ``` +### +### Clean up and check code by running +### +### ``` shell +### docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix +### docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff +### ``` +### +### > [!NOTE] The template adds `.php-cs-fixer.dist.php` as [a configuration +### > file for PHP CS +### > Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/config.rst) +### > and this makes it possible to override the actual configuration used in a +### > project by adding a more important configuration file, `.php-cs-fixer.php`. + +name: Symfony PHP + +env: + COMPOSE_USER: runner + +on: + pull_request: + paths: &paths + - "**/*.php" + - "composer.json" + - "composer.lock" + - "docker-compose.yml" + push: + branches: + - main + - develop + paths: *paths + +jobs: + coding-standards: + name: PHP - Check Coding Standards + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + # https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/usage.rst#the-check-command + docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff diff --git a/.github/workflows/styles.yaml b/.github/workflows/styles.yaml new file mode 100644 index 0000000..d3b4588 --- /dev/null +++ b/.github/workflows/styles.yaml @@ -0,0 +1,40 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/symfony/styles.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Symfony Styles (CSS and SCSS) +### +### Validates styles files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. + +name: Styles + +on: + pull_request: + paths: &paths + - "assets/**/*.css" + - "assets/**/*.scss" + - "docker-compose.yml" + push: + branches: + - main + - develop + paths: *paths + +jobs: + styles-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier 'assets/**/*.{css,scss}' --check diff --git a/.github/workflows/twig.yaml b/.github/workflows/twig.yaml new file mode 100644 index 0000000..52536c3 --- /dev/null +++ b/.github/workflows/twig.yaml @@ -0,0 +1,55 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/twig.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Twig +### +### Validates Twig files +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [vincentlanglet/twig-cs-fixer](https://github.com/VincentLanglet/Twig-CS-Fixer) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev vincentlanglet/twig-cs-fixer +### ``` +### +### 3. A [Configuration +### file](https://github.com/VincentLanglet/Twig-CS-Fixer/blob/main/docs/configuration.md#configuration-file) +### in the root of the project defines which files to check and rules to use. + +name: Twig + +env: + COMPOSE_USER: runner + +on: + pull_request: + paths: &paths + - "**/*.twig" + - "composer.json" + - "composer.lock" + - "docker-compose.yml" + push: + branches: + - main + - develop + paths: *paths + +jobs: + twig-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm vendor/bin/twig-cs-fixer lint diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml new file mode 100644 index 0000000..911097e --- /dev/null +++ b/.github/workflows/yaml.yaml @@ -0,0 +1,45 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/yaml.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### YAML +### +### Validates YAML files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. +### +### #### Symfony YAML +### +### Symfony's YAML config files use 4 spaces for indentation and single quotes. +### Therefore we use a [Prettier configuration +### file](https://prettier.io/docs/configuration), `.prettierrc.yaml`, to make +### Prettier format YAML files in the `config/` folder like Symfony expects. + +name: YAML + +on: + pull_request: + paths: &paths + - "**/*.yml" + - "**/*.yaml" + push: + branches: + - main + - develop + paths: *paths + +jobs: + yaml-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier '**/*.{yml,yaml}' --check diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..0253096 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,22 @@ +// This file is copied from config/markdown/.markdownlint.jsonc in https://github.com/itk-dev/devops_itkdev-docker. +// Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +// markdownlint-cli configuration file (cf. https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration) +{ + "default": true, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md + "line-length": { + "line_length": 120, + "code_blocks": false, + "tables": false + }, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md + "no-duplicate-heading": { + "siblings_only": true + }, + // https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections#creating-a-collapsed-section + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md + "no-inline-html": { + "allowed_elements": ["details", "summary"] + } +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..d143ace --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,12 @@ +# This file is copied from config/markdown/.markdownlintignore in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#ignoring-files +vendor/ +node_modules/ +LICENSE.md +# Drupal +web/*.md +web/core/ +web/libraries/ +web/*/contrib/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8827682 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +_misc + diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..a99aea9 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,18 @@ +# This file is copied from config/symfony/yaml/.prettierrc.yaml in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# https://prettier.io/docs/configuration +overrides: + # https://taskfile.dev/docs/styleguide + - files: + - "Taskfile.{yml,yaml}" + options: + tabWidth: 2 + singleQuote: true + + # Symfony config + - files: + - "config/**/*.{yml,yaml}" + options: + tabWidth: 4 + singleQuote: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bf48d94 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/itk-dev/enter diff --git a/README.md b/README.md index 8e132d2..95afbea 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ task site:update ddev launch ``` +Run `task` to see what cool task are available. Running `ddev` can help with other stuff. + ## Broker A [Scorpio Broker](https://scorpio.readthedocs.io/) is part of the development setup. @@ -38,7 +40,6 @@ ddev console app:broker:import:geojson hundeskov 'https://webkort.aarhuskommune. ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --get --data-urlencode type=hundeskov" | jq '.[]|with_entries(select([.key] | inside(["id", "type", "location", "geometry"])))' ``` - ### Broker API request examples @@ -117,7 +118,6 @@ ddev exec "curl --silent --header 'accept: application/geo+json' http://scorpio. * - --- * @@ -150,8 +150,12 @@ ddev console app:import:geojson toilet 'https://webkort.aarhuskommune.dk/spatial * [Public toilets in Aarhus Municipality](https://www.opendata.dk/city-of-aarhus/toiletter-i-aarhus-kommune) - * [Offentlige toiletter](http://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter): -* [Parking in Aarhus Municipality – Zones, Permits and Spaces](https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune) - * [Handicapparkering](https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap): + * [Offentlige + toiletter](http://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter): + +* [Parking in Aarhus Municipality – Zones, Permits and + Spaces](https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune) + * [Handicapparkering](https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap): + * diff --git a/Taskfile.yml b/Taskfile.yml index 400d1f1..49f5e72 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -23,15 +23,17 @@ tasks: test: desc: Run all tests cmds: - - task: test:application - - task: test:integration + - task: test:unit + # - task: test:application + # - task: test:integration - test:integration: &test_task + test:unit: &test_task env: APP_ENV: test desc: 'Run all {{.TASK | replace "test:" "" }} tests' cmd: ddev php bin/phpunit tests/{{.TASK | replace "test:" "" | title }} + test:integration: *test_task test:application: *test_task coding-standards:apply: @@ -108,38 +110,26 @@ tasks: coding-standards:php:apply: desc: 'Apply coding standards for PHP' cmds: - - task: compose - vars: - TASK_ARGS: exec phpfpm vendor/bin/rector - - task: compose - vars: - TASK_ARGS: exec phpfpm vendor/bin/php-cs-fixer fix + - ddev php vendor/bin/rector + - ddev php vendor/bin/php-cs-fixer fix coding-standards:php:check: desc: 'Apply and check coding standards for PHP' cmds: - task: coding-standards:php:apply - - task: compose - vars: - TASK_ARGS: exec phpfpm vendor/bin/rector --dry-run - - task: compose - vars: - TASK_ARGS: exec phpfpm vendor/bin/php-cs-fixer check + - ddev php vendor/bin/rector --dry-run + - ddev php vendor/bin/php-cs-fixer check coding-standards:twig:apply: desc: 'Apply coding standards for Twig' cmds: - - task: compose - vars: - TASK_ARGS: exec phpfpm vendor/bin/twig-cs-fixer fix + - ddev php vendor/bin/twig-cs-fixer fix coding-standards:twig:check: desc: 'Apply and check coding standards for Twig' cmds: - task: coding-standards:twig:apply - - task: compose - vars: - TASK_ARGS: exec phpfpm vendor/bin/twig-cs-fixer check + - ddev php vendor/bin/twig-cs-fixer check coding-standards:yaml:apply: desc: 'Apply coding standards for YAML' @@ -172,6 +162,16 @@ tasks: vars: TASK_ARGS: run --rm --no-deps phpfpm vendor/bin/phpstan + compose: + internal: true + cmd: 'docker compose {{.TASK_ARGS}} {{.CLI_ARGS}}' + + composer: + internal: true + task: compose + vars: + TASK_ARGS: run --rm --no-deps phpfpm composer + default: cmds: - task --list-all diff --git a/assets/app.js b/assets/app.js index d7879d7..d0d0685 100644 --- a/assets/app.js +++ b/assets/app.js @@ -4,8 +4,8 @@ * This file will be included onto the page via the importmap() Twig function, * which should already be in your base.html.twig. */ -import './styles/app.css'; -import WidgetAPI from 'septima-widget' +import "./styles/app.css"; +import WidgetAPI from "septima-widget"; // const widget = new WidgetAPI('#map', { // map: { diff --git a/composer.json b/composer.json index d50893f..b3bcf60 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,7 @@ { - "license": "proprietary", + "name": "itk-dev/enter", + "description": "Enter", + "license": "MIT", "type": "project", "require": { "php": ">=8.4", @@ -7,33 +9,33 @@ "ext-iconv": "*", "itamair/geophp": "^1.12", "phpdocumentor/reflection-docblock": "^6.0", - "phpstan/phpdoc-parser": "^2.3", + "phpstan/phpdoc-parser": "^2.3.5", "proj4php/proj4php": "^2.0", "symfony/asset": "~8.1.0", "symfony/asset-mapper": "~8.1.0", - "symfony/console": "~8.1.0", - "symfony/dotenv": "~8.1.0", + "symfony/console": "~8.1.6", + "symfony/dotenv": "~8.1.6", "symfony/flex": "^2", - "symfony/framework-bundle": "~8.1.0", - "symfony/http-client": "~8.1.0", + "symfony/framework-bundle": "~8.1.6", + "symfony/http-client": "~8.1.6", "symfony/property-access": "~8.1.0", - "symfony/property-info": "~8.1.0", + "symfony/property-info": "~8.1.6", "symfony/runtime": "~8.1.0", - "symfony/serializer": "~8.1.0", + "symfony/serializer": "~8.1.6", "symfony/twig-bundle": "~8.1.0", - "symfony/yaml": "~8.1.0", + "symfony/yaml": "~8.1.6", "twig/extra-bundle": "^2.12 || ^3.0", "twig/twig": "^2.12 || ^3.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.52", - "friendsofphp/php-cs-fixer": "^3.95", - "phpstan/phpstan": "^2.2", + "friendsofphp/php-cs-fixer": "^3.95.24", + "phpstan/phpstan": "^2.2.12", "phpunit/phpunit": "^13.3", "rector/rector": "^2.6", "symfony/browser-kit": "~8.1.0", - "symfony/css-selector": "~8.1.0", - "symfony/debug-bundle": "8.1.*", + "symfony/css-selector": "~8.1.6", + "symfony/debug-bundle": "~8.1.0", "symfony/maker-bundle": "^1.67", "symfony/stopwatch": "~8.1.0", "symfony/web-profiler-bundle": "~8.1.0", diff --git a/composer.lock b/composer.lock index 782be9a..2b3d45f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5bd73ae1836b8ed57dcd7a8086881f80", + "content-hash": "89753fcad5de669970a5e88087a1a523", "packages": [ { "name": "composer/semver", @@ -356,16 +356,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.3", + "version": "2.3.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/148cefffaf0233e4c08cc13db8a195a56dd6dfe9", + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9", "shasum": "" }, "require": { @@ -397,9 +397,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.5" }, - "time": "2026-07-08T07:01:06+00:00" + "time": "2026-08-31T16:05:28+00:00" }, { "name": "proj4php/proj4php", @@ -820,16 +820,16 @@ }, { "name": "symfony/cache", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "c12be586c0e4798e649e7501aa9181ddc9d93310" + "reference": "91a2b338b18de1400ba38faae3f1e160d3eefc28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/c12be586c0e4798e649e7501aa9181ddc9d93310", - "reference": "c12be586c0e4798e649e7501aa9181ddc9d93310", + "url": "https://api.github.com/repos/symfony/cache/zipball/91a2b338b18de1400ba38faae3f1e160d3eefc28", + "reference": "91a2b338b18de1400ba38faae3f1e160d3eefc28", "shasum": "" }, "require": { @@ -895,7 +895,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.1.5" + "source": "https://github.com/symfony/cache/tree/v8.1.6" }, "funding": [ { @@ -915,7 +915,7 @@ "type": "tidelift" } ], - "time": "2026-08-19T08:32:28+00:00" + "time": "2026-08-30T20:10:55+00:00" }, { "name": "symfony/cache-contracts", @@ -1077,16 +1077,16 @@ }, { "name": "symfony/console", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d07c06839e33047e2c894a6793248f3fb66c8129" + "reference": "eb7d9957d66739649e931ce7a9d05dab69f8abac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d07c06839e33047e2c894a6793248f3fb66c8129", - "reference": "d07c06839e33047e2c894a6793248f3fb66c8129", + "url": "https://api.github.com/repos/symfony/console/zipball/eb7d9957d66739649e931ce7a9d05dab69f8abac", + "reference": "eb7d9957d66739649e931ce7a9d05dab69f8abac", "shasum": "" }, "require": { @@ -1153,7 +1153,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.1.5" + "source": "https://github.com/symfony/console/tree/v8.1.6" }, "funding": [ { @@ -1173,7 +1173,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T14:29:57+00:00" + "time": "2026-08-25T14:18:42+00:00" }, { "name": "symfony/dependency-injection", @@ -1329,7 +1329,7 @@ }, { "name": "symfony/dotenv", - "version": "v8.1.2", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", @@ -1379,7 +1379,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v8.1.2" + "source": "https://github.com/symfony/dotenv/tree/v8.1.6" }, "funding": [ { @@ -1650,16 +1650,16 @@ }, { "name": "symfony/filesystem", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6b2f4a0eeb28b5d74f90862592923a654bc629b3" + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6b2f4a0eeb28b5d74f90862592923a654bc629b3", - "reference": "6b2f4a0eeb28b5d74f90862592923a654bc629b3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7599ebb855fede59413ddb7f15198d67bfcae7ba", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba", "shasum": "" }, "require": { @@ -1697,7 +1697,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.1.5" + "source": "https://github.com/symfony/filesystem/tree/v8.1.6" }, "funding": [ { @@ -1717,7 +1717,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T12:16:08+00:00" + "time": "2026-08-23T10:06:25+00:00" }, { "name": "symfony/finder", @@ -1862,16 +1862,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "1c7b41364312c67376a560e35c7f7f0be0c1b4ea" + "reference": "18ea259ef00ecbd13ce87fcbed3978758154e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/1c7b41364312c67376a560e35c7f7f0be0c1b4ea", - "reference": "1c7b41364312c67376a560e35c7f7f0be0c1b4ea", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/18ea259ef00ecbd13ce87fcbed3978758154e5a0", + "reference": "18ea259ef00ecbd13ce87fcbed3978758154e5a0", "shasum": "" }, "require": { @@ -1982,7 +1982,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v8.1.5" + "source": "https://github.com/symfony/framework-bundle/tree/v8.1.6" }, "funding": [ { @@ -2002,20 +2002,20 @@ "type": "tidelift" } ], - "time": "2026-08-21T19:39:55+00:00" + "time": "2026-08-30T20:10:55+00:00" }, { "name": "symfony/http-client", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "9f941ed000bb11f16dc7eafed98a7b646d3b3e5d" + "reference": "fe91dd1ddf09b61aa01e73d29907acbf1775c0a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/9f941ed000bb11f16dc7eafed98a7b646d3b3e5d", - "reference": "9f941ed000bb11f16dc7eafed98a7b646d3b3e5d", + "url": "https://api.github.com/repos/symfony/http-client/zipball/fe91dd1ddf09b61aa01e73d29907acbf1775c0a9", + "reference": "fe91dd1ddf09b61aa01e73d29907acbf1775c0a9", "shasum": "" }, "require": { @@ -2079,7 +2079,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v8.1.5" + "source": "https://github.com/symfony/http-client/tree/v8.1.6" }, "funding": [ { @@ -2099,20 +2099,20 @@ "type": "tidelift" } ], - "time": "2026-08-21T17:47:34+00:00" + "time": "2026-08-30T14:03:40+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.7.1", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "41fc42d276aeff21192465331ebbab7d83a743c0" + "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41fc42d276aeff21192465331ebbab7d83a743c0", - "reference": "41fc42d276aeff21192465331ebbab7d83a743c0", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/35be0019e2c2c9fba80f9dc033290a5240f7b44f", + "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f", "shasum": "" }, "require": { @@ -2161,7 +2161,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.3" }, "funding": [ { @@ -2181,20 +2181,20 @@ "type": "tidelift" } ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-08-04T08:41:16+00:00" }, { "name": "symfony/http-foundation", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4" + "reference": "093b78326f649c3a9db922b9f17123b6aeb3b8fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee16f97e95cfa011a742714d7c8c8f70fe7423f4", - "reference": "ee16f97e95cfa011a742714d7c8c8f70fe7423f4", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/093b78326f649c3a9db922b9f17123b6aeb3b8fb", + "reference": "093b78326f649c3a9db922b9f17123b6aeb3b8fb", "shasum": "" }, "require": { @@ -2242,7 +2242,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v8.1.5" + "source": "https://github.com/symfony/http-foundation/tree/v8.1.6" }, "funding": [ { @@ -2262,20 +2262,20 @@ "type": "tidelift" } ], - "time": "2026-08-20T09:59:12+00:00" + "time": "2026-08-30T20:10:55+00:00" }, { "name": "symfony/http-kernel", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6" + "reference": "2f73beb7c6f1a97d2c17bbf4dbd59da8cc18b355" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/0306e1e65b90023fe40de6c6be95d06396bcb2e6", - "reference": "0306e1e65b90023fe40de6c6be95d06396bcb2e6", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2f73beb7c6f1a97d2c17bbf4dbd59da8cc18b355", + "reference": "2f73beb7c6f1a97d2c17bbf4dbd59da8cc18b355", "shasum": "" }, "require": { @@ -2352,7 +2352,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v8.1.5" + "source": "https://github.com/symfony/http-kernel/tree/v8.1.6" }, "funding": [ { @@ -2372,7 +2372,7 @@ "type": "tidelift" } ], - "time": "2026-08-22T13:45:00+00:00" + "time": "2026-08-30T21:40:49+00:00" }, { "name": "symfony/polyfill-deepclone", @@ -2876,16 +2876,16 @@ }, { "name": "symfony/property-info", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "b335f8e7fb1440ed3448fb33340efc6127678e53" + "reference": "d25e1dabbaff3f91bb15ba68328cf4fbd1c43c42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/b335f8e7fb1440ed3448fb33340efc6127678e53", - "reference": "b335f8e7fb1440ed3448fb33340efc6127678e53", + "url": "https://api.github.com/repos/symfony/property-info/zipball/d25e1dabbaff3f91bb15ba68328cf4fbd1c43c42", + "reference": "d25e1dabbaff3f91bb15ba68328cf4fbd1c43c42", "shasum": "" }, "require": { @@ -2938,7 +2938,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v8.1.5" + "source": "https://github.com/symfony/property-info/tree/v8.1.6" }, "funding": [ { @@ -2958,11 +2958,11 @@ "type": "tidelift" } ], - "time": "2026-08-21T17:47:34+00:00" + "time": "2026-08-30T08:48:43+00:00" }, { "name": "symfony/routing", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -3018,7 +3018,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.1.5" + "source": "https://github.com/symfony/routing/tree/v8.1.6" }, "funding": [ { @@ -3126,16 +3126,16 @@ }, { "name": "symfony/serializer", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "9c064d505c661e3e17aa8999870f4fb0ed05e024" + "reference": "931151b12d6e2d4996ce2ce8e7b180d35455f37a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/9c064d505c661e3e17aa8999870f4fb0ed05e024", - "reference": "9c064d505c661e3e17aa8999870f4fb0ed05e024", + "url": "https://api.github.com/repos/symfony/serializer/zipball/931151b12d6e2d4996ce2ce8e7b180d35455f37a", + "reference": "931151b12d6e2d4996ce2ce8e7b180d35455f37a", "shasum": "" }, "require": { @@ -3201,7 +3201,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v8.1.5" + "source": "https://github.com/symfony/serializer/tree/v8.1.6" }, "funding": [ { @@ -3221,20 +3221,20 @@ "type": "tidelift" } ], - "time": "2026-08-22T09:06:25+00:00" + "time": "2026-08-30T08:48:47+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.7.1", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", "shasum": "" }, "require": { @@ -3288,7 +3288,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" }, "funding": [ { @@ -3308,7 +3308,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T09:55:08+00:00" + "time": "2026-07-27T15:39:01+00:00" }, { "name": "symfony/string", @@ -3845,16 +3845,16 @@ }, { "name": "symfony/var-exporter", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1" + "reference": "802362f41128c430fd6685491e1fb72127fae78a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", - "reference": "b8f7dd85493e8372c7c81a1547caaaf86b9c16d1", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/802362f41128c430fd6685491e1fb72127fae78a", + "reference": "802362f41128c430fd6685491e1fb72127fae78a", "shasum": "" }, "require": { @@ -3904,7 +3904,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v8.1.5" + "source": "https://github.com/symfony/var-exporter/tree/v8.1.6" }, "funding": [ { @@ -3924,20 +3924,20 @@ "type": "tidelift" } ], - "time": "2026-08-13T16:11:28+00:00" + "time": "2026-08-29T06:30:03+00:00" }, { "name": "symfony/yaml", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd" + "reference": "0b4aa53a67f9fece88c665f1a1dadcfd25d93fe5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", - "reference": "b3fc9e8888eeb9daddc33bfbd15d282a61f543cd", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0b4aa53a67f9fece88c665f1a1dadcfd25d93fe5", + "reference": "0b4aa53a67f9fece88c665f1a1dadcfd25d93fe5", "shasum": "" }, "require": { @@ -3980,7 +3980,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.1.5" + "source": "https://github.com/symfony/yaml/tree/v8.1.6" }, "funding": [ { @@ -4000,7 +4000,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T12:16:08+00:00" + "time": "2026-08-30T01:03:44+00:00" }, { "name": "twig/extra-bundle", @@ -5165,16 +5165,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.95.23", + "version": "v3.95.24", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "b2932a5af3157a0c28324b1efe5e3b556dee09c4" + "reference": "9a4b8050f8768646bf3693ee7416978ceeca6206" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b2932a5af3157a0c28324b1efe5e3b556dee09c4", - "reference": "b2932a5af3157a0c28324b1efe5e3b556dee09c4", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9a4b8050f8768646bf3693ee7416978ceeca6206", + "reference": "9a4b8050f8768646bf3693ee7416978ceeca6206", "shasum": "" }, "require": { @@ -5257,7 +5257,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.23" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.24" }, "funding": [ { @@ -5265,7 +5265,7 @@ "type": "github" } ], - "time": "2026-08-26T19:54:54+00:00" + "time": "2026-08-31T15:31:12+00:00" }, { "name": "justinrainbow/json-schema", @@ -5697,11 +5697,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.10", + "version": "2.2.12", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/36d1509c998b0602811824143526b4a9ee2774e7", - "reference": "36d1509c998b0602811824143526b4a9ee2774e7", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/174b0d88710f00a42598886504dd7a146f91ace5", + "reference": "174b0d88710f00a42598886504dd7a146f91ace5", "shasum": "" }, "require": { @@ -5757,7 +5757,7 @@ "type": "github" } ], - "time": "2026-08-30T12:46:16+00:00" + "time": "2026-08-31T19:09:43+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8049,16 +8049,16 @@ }, { "name": "symfony/css-selector", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "a291fb5adb65f52a4bb315db2d803698315dc64d" + "reference": "08e2905152a39cf3fd1745d83f8c483e258887d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/a291fb5adb65f52a4bb315db2d803698315dc64d", - "reference": "a291fb5adb65f52a4bb315db2d803698315dc64d", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/08e2905152a39cf3fd1745d83f8c483e258887d9", + "reference": "08e2905152a39cf3fd1745d83f8c483e258887d9", "shasum": "" }, "require": { @@ -8094,7 +8094,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v8.1.5" + "source": "https://github.com/symfony/css-selector/tree/v8.1.6" }, "funding": [ { @@ -8114,7 +8114,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T17:47:34+00:00" + "time": "2026-08-23T10:06:25+00:00" }, { "name": "symfony/debug-bundle", @@ -8433,7 +8433,7 @@ }, { "name": "symfony/process", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -8474,7 +8474,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.1.5" + "source": "https://github.com/symfony/process/tree/v8.1.6" }, "funding": [ { diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml index 6899b72..c3eb53d 100644 --- a/config/packages/cache.yaml +++ b/config/packages/cache.yaml @@ -16,4 +16,4 @@ framework: # Namespaced pools use the above "app" backend by default #pools: - #my.dedicated.cache: null + #my.dedicated.cache: null diff --git a/config/packages/debug.yaml b/config/packages/debug.yaml index ad874af..54a4821 100644 --- a/config/packages/debug.yaml +++ b/config/packages/debug.yaml @@ -2,4 +2,4 @@ when@dev: debug: # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. # See the "server:dump" command to start a new server. - dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" + dump_destination: 'tcp://%env(VAR_DUMPER_SERVER)%' diff --git a/config/services.yaml b/config/services.yaml index 79b8ce2..7adddb9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -11,7 +11,7 @@ parameters: services: # default configuration for services in *this* file _defaults: - autowire: true # Automatically injects dependencies in your services. + autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. # makes classes in src/ available to be used as services diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..265ede4 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,27 @@ +# itk-version: 3.2.4 +services: + phpfpm: + environment: + - PHP_SENDMAIL_PATH=/usr/sbin/sendmail -S mail:1025 + + nginx: + labels: + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}.middlewares=ITKBasicAuth@file" + + mail: + image: axllent/mailpit + restart: unless-stopped + networks: + - app + - frontend + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME:?}-http.rule=Host(`mail.${COMPOSE_SERVER_DOMAIN:?}`)" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME:?}-http.entrypoints=web" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME:?}-http.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME:?}.rule=Host(`mail.${COMPOSE_SERVER_DOMAIN:?}`)" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME:?}.entrypoints=websecure" + - "traefik.http.services.mail_${COMPOSE_PROJECT_NAME:?}.loadbalancer.server.port=8025" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME:?}.middlewares=ITKMailhogAuth@file" diff --git a/docker-compose.redirect.yml b/docker-compose.redirect.yml new file mode 100644 index 0000000..4710035 --- /dev/null +++ b/docker-compose.redirect.yml @@ -0,0 +1,15 @@ +# itk-version: 3.2.4 +services: + nginx: + labels: + # Add www before domain and set redirect to non-www + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME:?}-http.rule=Host(`www.${COMPOSE_SERVER_DOMAIN:?}`)" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME:?}-http.entrypoints=web" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME:?}-http.middlewares=redirect-to-https,non_www" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME:?}.rule=Host(`www.${COMPOSE_SERVER_DOMAIN:?}`)" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME:?}.entrypoints=websecure" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME:?}.middlewares=non_www" + + - traefik.http.middlewares.non_www.redirectregex.regex=^(http|https)?://(?:www\.)?(.+) + - traefik.http.middlewares.non_www.redirectregex.replacement=https://$${2:?} + - traefik.http.middlewares.non_www.redirectregex.permanent=true diff --git a/docker-compose.server.yml b/docker-compose.server.yml new file mode 100644 index 0000000..75148ed --- /dev/null +++ b/docker-compose.server.yml @@ -0,0 +1,52 @@ +# itk-version: 3.2.4 +networks: + frontend: + external: true + app: + driver: bridge + internal: false + +services: + phpfpm: + image: itkdev/php8.4-fpm:alpine + restart: unless-stopped + networks: + - app + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - PHP_MAX_EXECUTION_TIME=30 + - PHP_MEMORY_LIMIT=128M + volumes: + - .:/app + + nginx: + image: nginxinc/nginx-unprivileged:alpine + restart: unless-stopped + networks: + - app + - frontend + depends_on: + - phpfpm + volumes: + - ./.docker/templates:/etc/nginx/templates:ro + - ./.docker/nginx.conf:/etc/nginx/nginx.conf:ro + - .:/app + environment: + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME:?}-phpfpm-1:9000 + NGINX_CRON_METRICS: ${COMPOSE_PROJECT_NAME:?}-phpfpm-1:9746 + NGINX_WEB_ROOT: /app/public + NGINX_PORT: 8080 + NGINX_MAX_BODY_SIZE: 5M + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}-http.rule=Host(`${COMPOSE_SERVER_DOMAIN:?}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}-http.entrypoints=web" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}-http.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}.rule=Host(`${COMPOSE_SERVER_DOMAIN:?}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}.entrypoints=websecure" + # Cron-metrics protection. + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}-metrics.rule=Host(`${COMPOSE_SERVER_DOMAIN:?}`) && PathPrefix(`/cron-metrics`) " + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}-metrics.middlewares=ITKMetricsAuth@file" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4c8f69a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,110 @@ +# itk-version: 3.2.4 +networks: + frontend: + external: true + app: + driver: bridge + internal: false + +services: + mariadb: + image: itkdev/mariadb:latest + networks: + - app + ports: + - "3306" + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 10s + interval: 10s + timeout: 5s + retries: 3 + environment: + - MYSQL_ROOT_PASSWORD=password + - MYSQL_USER=db + - MYSQL_PASSWORD=db + - MYSQL_DATABASE=db + #- ENCRYPT=1 # Uncomment to enable database encryption. + + phpfpm: + image: itkdev/php8.4-fpm:latest + user: ${COMPOSE_USER:-deploy} + networks: + - app + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - PHP_XDEBUG_MODE=${PHP_XDEBUG_MODE:-off} + - PHP_MAX_EXECUTION_TIME=30 + - PHP_MEMORY_LIMIT=256M + # Depending on the setup, you may have to remove --read-envelope-from from msmtp (cf. https://marlam.de/msmtp/msmtp.html) or use SMTP to send mail + - PHP_SENDMAIL_PATH=/usr/bin/msmtp --host=mail --port=1025 --read-recipients --read-envelope-from + - DOCKER_HOST_DOMAIN=${COMPOSE_DOMAIN:?} + - PHP_IDE_CONFIG=serverName=localhost + depends_on: + mariadb: + condition: service_healthy + volumes: + - .:/app + + nginx: + image: nginxinc/nginx-unprivileged:alpine + networks: + - app + - frontend + depends_on: + - phpfpm + ports: + - "8080" + volumes: + - ./.docker/templates:/etc/nginx/templates:ro + - .:/app + environment: + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME:?}-phpfpm-1:9000 + NGINX_CRON_METRICS: ${COMPOSE_PROJECT_NAME:?}-phpfpm-1:9746 + NGINX_WEB_ROOT: /app/public + NGINX_PORT: 8080 + NGINX_MAX_BODY_SIZE: 5M + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}.rule=Host(`${COMPOSE_DOMAIN:?}`)" + # HTTPS config - uncomment to enable redirect from :80 to :443 + # - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}.middlewares=redirect-to-https" + # - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + # Cron-metrics protection. + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}-metrics.rule=Host(`${COMPOSE_DOMAIN:?}`) && PathPrefix(`/cron-metrics`) " + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}-metrics.middlewares=ITKMetricsAuth@file" + + mail: + image: axllent/mailpit + networks: + - app + - frontend + ports: + - "1025" + - "8025" + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:?}mail.rule=Host(`mail-${COMPOSE_DOMAIN:?}`)" + - "traefik.http.services.${COMPOSE_PROJECT_NAME:?}mail.loadbalancer.server.port=8025" + + # Code checks tools + markdownlint: + image: itkdev/markdownlint + profiles: + - dev + volumes: + - ./:/md + + prettier: + # Prettier does not (yet, fcf. + # https://github.com/prettier/prettier/issues/15206) have an official + # docker image. + # https://hub.docker.com/r/jauderho/prettier is good candidate (cf. https://hub.docker.com/search?q=prettier&sort=updated_at&order=desc) + image: jauderho/prettier + profiles: + - dev + volumes: + - ./:/work diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 23c7172..38793b8 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -15,6 +15,6 @@ parameters: path: src/Command/BrokerImportGeoJson.php - messages: - - '#contains generic class RecursiveArrayIterator but does not specify its types#' - - '#Call to an undefined method RecursiveIterator::getArrayCopy\(\).#' + - "#contains generic class RecursiveArrayIterator but does not specify its types#" + - '#Call to an undefined method RecursiveIterator::getArrayCopy\(\).#' path: src/GeometryHelper.php diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..18a3d91 --- /dev/null +++ b/rector.php @@ -0,0 +1,18 @@ +withPaths([ + __DIR__.'/assets', + __DIR__.'/config', + __DIR__.'/src', + __DIR__.'/tests', + ]) + // uncomment to reach your current PHP version + ->withPhpSets(php85: true) + ->withTypeCoverageLevel(0) + ->withDeadCodeLevel(0) + ->withCodeQualityLevel(0); diff --git a/src/Command/BrokerEntityDelete.php b/src/Command/BrokerEntityDelete.php index 98bba64..d1da87d 100644 --- a/src/Command/BrokerEntityDelete.php +++ b/src/Command/BrokerEntityDelete.php @@ -21,14 +21,13 @@ class BrokerEntityDelete * @param string[]|null $entityTypes */ public function __invoke( - SymfonyStyle $io, + SymfonyStyle $io, HttpClientInterface $brokerClient, #[Argument(name: 'type')] - ?array $entityTypes = null, + ?array $entityTypes = null, #[Option] - bool $all = false, - ): int - { + bool $all = false, + ): int { if ($all) { $data = $brokerClient->request(Request::METHOD_GET, '/ngsi-ld/v1/types')->toArray(); $entityTypes = $data['typeList'] ?? []; @@ -59,7 +58,7 @@ public function __invoke( }); $io->progressStart($count); foreach ($entities as $entity) { - $brokerClient->request(Request::METHOD_DELETE, '/ngsi-ld/v1/entities/' . urlencode($entity['id'])) + $brokerClient->request(Request::METHOD_DELETE, '/ngsi-ld/v1/entities/'.urlencode($entity['id'])) ->getStatusCode(); $io->progressAdvance(); } @@ -84,7 +83,7 @@ private function getLinkUrl(ResponseInterface $response, string $rel): ?string { $links = $response->getHeaders()['link'] ?? []; foreach ($links as $link) { - if (preg_match_all('/<(?P[^>]+)>;\s*rel="(?P[^"]+)"/', (string)$link, $matches, PREG_SET_ORDER)) { + if (preg_match_all('/<(?P[^>]+)>;\s*rel="(?P[^"]+)"/', (string) $link, $matches, PREG_SET_ORDER)) { if ($rel === ($matches[0]['rel'] ?? null)) { return $matches[0]['url'] ?? null; } diff --git a/src/Command/BrokerImportGeoJson.php b/src/Command/BrokerImportGeoJson.php index b70ad4f..da2839d 100644 --- a/src/Command/BrokerImportGeoJson.php +++ b/src/Command/BrokerImportGeoJson.php @@ -31,9 +31,6 @@ public function __invoke( #[Argument()] string $dataUrl, ): int { - // $entityType = 'toilet'; - // // https://www.opendata.dk/city-of-aarhus/toiletter-i-aarhus-kommune - // $dataUrl = 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter'; $data = $httpClient->request(Request::METHOD_GET, $dataUrl)->toArray(); $features = $data['features'] ?? null; $sourceProjection = $data['crs']['properties']['name'] ?? null; @@ -53,11 +50,6 @@ public function __invoke( 'type' => $entityType, 'id' => sprintf('%s:%04d', $entityType, $index), ]; -// foreach (['geometry'] as $key) { -// if (array_key_exists($key, $feature)) { -// $entity[self::RESERVED_PROPERTY_NAME_PREFIX.$key] = $feature[$key]; -// } -// } foreach ($feature['properties'] as $name => $value) { if (null === $value) { continue; diff --git a/src/Controller/DataController.php b/src/Controller/DataController.php index 38766ad..25ccb2f 100644 --- a/src/Controller/DataController.php +++ b/src/Controller/DataController.php @@ -26,8 +26,8 @@ final class DataController extends AbstractController ], defaults: ['_format' => self::FORMAT_JSON], )] - public function index(Request $request, string $path, string $_format, - HttpClientInterface $brokerClient, + public function index(Request $request, string $path, string $_format, + HttpClientInterface $brokerClient, ): JsonResponse { $path = '/'.ltrim($path, '/'); $headers = $request->headers->all(); diff --git a/src/GeometryHelper.php b/src/GeometryHelper.php index 05006c4..2af9f14 100644 --- a/src/GeometryHelper.php +++ b/src/GeometryHelper.php @@ -56,7 +56,7 @@ public function transformGeoJsonGeometry(array $geometry, Crs|string|null $from, { $arrayIterator = new \RecursiveArrayIterator($geometry); $iterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::CHILD_FIRST); - foreach ($iterator as $key => $value) { + foreach ($iterator as $value) { if (is_array($value) && array_is_list($value) && 2 === count($value) && is_float($value[0]) && is_float($value[1])) { $transformed = $this->transformCoordinates($value[0], $value[1], $to, $from); // Am I stupid or is PHP stupid? diff --git a/templates/default/hundeskov.html.twig b/templates/default/hundeskov.html.twig index 611eb6b..32e3e34 100644 --- a/templates/default/hundeskov.html.twig +++ b/templates/default/hundeskov.html.twig @@ -50,7 +50,7 @@ debug: true, }, #} { - "features_host": {{ path('app_data', {path: '/ngsi-ld/v1/entities', _format: 'geojson', 'type': 'hundeskov', limit: 10})|json_encode|raw}}, + "features_host": {{ path('app_data', {path: '/ngsi-ld/v1/entities', _format: 'geojson', type: 'hundeskov', limit: 10})|json_encode|raw }}, // "features_template": "{ 'type': 'Feature', 'properties': <%= properties %>, 'geometry': <%= _formatJSON(location) %>}", @@ -75,7 +75,7 @@ debug: true, }, { - "features_host": {{ path('app_data', {path: '/ngsi-ld/v1/entities', _format: 'geojson', 'type': 'hundeskov', offset: 10, limit: 10})|json_encode|raw}}, + "features_host": {{ path('app_data', {path: '/ngsi-ld/v1/entities', _format: 'geojson', type: 'hundeskov', offset: 10, limit: 10})|json_encode|raw }}, // type: "json", // features_dataPath: "features", // features_template_row: "features", diff --git a/templates/default/index.html.twig b/templates/default/index.html.twig index af084f0..248b9b6 100644 --- a/templates/default/index.html.twig +++ b/templates/default/index.html.twig @@ -2,9 +2,8 @@ {% block content %} {% for path in ['hundeskov'] %} - {{ path }} - {% endfor %} - + {{ path }} + {% endfor %}
#map
@@ -53,11 +52,10 @@ Scorpio complains about "Bad Request Data." and says "Georelation is not valid" Reset
-{##} +{# #} {# https://github.com/Septima/widget-issues/issues/24#issuecomment-5478400091 #} https://enter.ddev.site:33001/data/ngsi-ld/v1/entities.geojson?georel=near;maxDistance==2000&geometry=Point&coordinates=[8,40] - {% endblock %} diff --git a/tests/GeometryHelperTest.php b/tests/Unit/GeometryHelperTest.php similarity index 88% rename from tests/GeometryHelperTest.php rename to tests/Unit/GeometryHelperTest.php index 000e9dc..5aa9455 100644 --- a/tests/GeometryHelperTest.php +++ b/tests/Unit/GeometryHelperTest.php @@ -1,6 +1,6 @@ transformCoordinates($input[0], $input[1], GeometryHelper::EPSG_GEO_JSON, GeometryHelper::EPSG_25832); + $actual = $helper->transformCoordinates($input[0], $input[1], GeometryHelper\Crs::geoJson(), GeometryHelper\Crs::EPSG_25832); $this->assertEquals($expected, $actual); } @@ -30,7 +30,7 @@ public function testTransformPoint(): void $expected = new Point( 10.236808, 56.101227, ); - $actual = $helper->transformPoint($input, GeometryHelper::EPSG_GEO_JSON, GeometryHelper::EPSG_25832); + $actual = $helper->transformPoint($input, GeometryHelper\Crs::geoJson(), GeometryHelper\Crs::EPSG_25832); $this->assertEquals($expected, $actual); } @@ -55,7 +55,7 @@ public function testTransformGeoJsonGeometry(): void ], ], ]; - $actual = $helper->transformGeoJsonGeometry($input, from: GeometryHelper::EPSG_25832, to: GeometryHelper::EPSG_GEO_JSON); + $actual = $helper->transformGeoJsonGeometry($input, from: GeometryHelper\Crs::EPSG_25832, to: GeometryHelper\Crs::EPSG_4326); $this->assertSame($expected, $actual); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 47a5855..b466cbe 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,7 +5,7 @@ require dirname(__DIR__).'/vendor/autoload.php'; if (method_exists(Dotenv::class, 'bootEnv')) { - (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); + new Dotenv()->bootEnv(dirname(__DIR__).'/.env'); } if ($_SERVER['APP_DEBUG']) { From 9f59e7e8d1654bde06ce8e8a11621393d9ba0561 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 1 Sep 2026 12:17:00 +0200 Subject: [PATCH 005/101] Added server setup --- .gitignore | 1 + docker-compose.server.override.yml | 36 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 docker-compose.server.override.yml diff --git a/.gitignore b/.gitignore index fbd3470..c7fb02c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .ddev/data .idea +*.local.yml ###> symfony/framework-bundle ### /.env.local diff --git a/docker-compose.server.override.yml b/docker-compose.server.override.yml new file mode 100644 index 0000000..9a3a124 --- /dev/null +++ b/docker-compose.server.override.yml @@ -0,0 +1,36 @@ +networks: + scorpio: + +# https://docs.ddev.com/en/stable/users/extend/custom-docker-services/ +services: + phpfpm: + image: itkdev/php8.5-fpm:alpine + + scorpio: + hostname: scorpio.local + image: scorpiobroker/all-in-one-runner:java-latest + restart: "no" + ports: + - "9090" + environment: + - DBHOST=scorpio-db + depends_on: + - "scorpio-db" + networks: + - app + - scorpio + platform: "linux/amd64" + + scorpio-db: + image: postgis/postgis + environment: + - POSTGRES_USER=ngb + - POSTGRES_PASSWORD=ngb + - POSTGRES_DB=ngb + networks: + - scorpio + volumes: + - ./.docker/data/scorpio-db:/var/lib/postgresql/data + platform: "linux/amd64" + # https://stackoverflow.com/a/58806511 + command: ["postgres", "-c", "log_statement=all"] From ecaba9cdfe3d66169e0a32f5166c980cade258f2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:20:37 +0200 Subject: [PATCH 006/101] Added ADR folder along with ADRs existing so far --- docs/adr/001-architecture-symfony-docker.md | 87 ++++++++++++ docs/adr/002-publish-to-a-context-broker.md | 104 +++++++++++++++ docs/adr/003-ngsi-ld-representation.md | 70 ++++++++++ docs/adr/004-coordinate-reference-system.md | 112 ++++++++++++++++ .../005-smart-data-models-as-vocabulary.md | 126 ++++++++++++++++++ .../006-onstreetparking-over-parkinggroup.md | 104 +++++++++++++++ docs/adr/CLAUDE.md | 78 +++++++++++ docs/adr/README.md | 20 +++ 8 files changed, 701 insertions(+) create mode 100644 docs/adr/001-architecture-symfony-docker.md create mode 100644 docs/adr/002-publish-to-a-context-broker.md create mode 100644 docs/adr/003-ngsi-ld-representation.md create mode 100644 docs/adr/004-coordinate-reference-system.md create mode 100644 docs/adr/005-smart-data-models-as-vocabulary.md create mode 100644 docs/adr/006-onstreetparking-over-parkinggroup.md create mode 100644 docs/adr/CLAUDE.md create mode 100644 docs/adr/README.md diff --git a/docs/adr/001-architecture-symfony-docker.md b/docs/adr/001-architecture-symfony-docker.md new file mode 100644 index 0000000..c0f8708 --- /dev/null +++ b/docs/adr/001-architecture-symfony-docker.md @@ -0,0 +1,87 @@ +# 001: Architecture — Symfony 8 on the ITK Dev Docker template + +| Field | Value | +|--------------------|----------------------------------------| +| **Created By** | Jeppe Krogh | +| **Date** | 2026-08-24 | +| **Decision Maker** | ITK Dev team | +| **Stakeholders** | ITK Dev developers, future maintainers | +| **Status** | Draft | + +## Context + +The adapter reads open data sets, converts them to a standard smart-city +representation, and publishes them to a context broker. It needs a runtime, an +HTTP client, a console for running imports, and a local development environment +including a broker to import into. It has no web UI and no domain data of its +own. + +ITK Dev maintains a fleet of PHP services with an established Docker-based +development convention, expressed as versioned project templates with shared CI +and coding-standards configuration. A new application either adopts that or +diverges from it. + +This ADR serves to decide the runtime, framework and development +environment the application is built on. + +### Drivers + +- **Functional:** scheduled console commands; outbound HTTP; a local broker. + No database and no HTTP surface of its own. +- **Non-functional:** minimal onboarding cost; shared tooling rather than + reimplemented tooling; reproducible across developers and CI; long-term + vendor support. + +### Options Considered + +1. **PHP 8.4 / Symfony 8 on the ITK Dev `symfony-8` template.** Matches the + organisation's existing stack, so CI, coding standards and task runner come + for free; the console component suits scheduled imports. Provisions a web + server, database and mail catcher this application never uses, and its PHP + version runs ahead of developer hosts, making containers mandatory. +2. **Minimal framework project without the template, run on the host.** No + unused services, no container requirement for the application — but shared + CI and coding-standards config would be reimplemented by hand, and a local + broker needs containers anyway, so the dependency is moved rather than + removed. +3. **A second entry point in an existing internal application.** One + deployment to operate, but couples a batch importer's release cycle to a + user-facing application and inherits dependencies it has no use for. +4. **A different language ecosystem on a bespoke setup.** Richer geospatial + libraries in some ecosystems, but no internal expertise and no shared + tooling. The transformations needed are available as mature libraries in the + established stack too. + +## Decision + +**PHP 8.4 + Symfony 8** on the ITK Dev `symfony-8` template, as its **own +deployable service**, with a containerised broker overlay for local development. + +- Standardising costs less over the application's lifetime than trimming unused + services. A second toolchain must be learned and patched; idle containers + cost only disk. +- A batch importer and a user-facing application have different lifecycles and + failure modes, so they stay separate services. +- No domain persistence is needed — the broker is the system of record — so the + template's database service is left unused rather than removed, keeping + template updates a clean diff. +- Local development includes a real broker, so imports are verified end to end + rather than only as serialised output. + +## Consequences + +### Positive + +- Onboarding cost close to zero; CI and coding standards work from the first + commit. +- No schema, no migrations, no state to keep consistent with the broker. + +### Negative / Trade-offs + +- **Containers are mandatory.** The template's PHP runs ahead of developer + hosts, so dependency management, console commands and tests cannot run + natively. Most likely source of first-run confusion. +- A web server, database and mail catcher are provisioned and never used. +- Broker images are not published for every CPU architecture, so local start-up + may be slow under emulation. +- The application follows the template's choices; deviating later has a cost. diff --git a/docs/adr/002-publish-to-a-context-broker.md b/docs/adr/002-publish-to-a-context-broker.md new file mode 100644 index 0000000..7372479 --- /dev/null +++ b/docs/adr/002-publish-to-a-context-broker.md @@ -0,0 +1,104 @@ +# 002: Publication mechanism — publish to a context broker + +| Field | Value | +|--------------------|--------------------------------------------------------| +| **Created By** | Jeppe Krogh | +| **Date** | 2026-08-24 | +| **Decision Maker** | ITK Dev team | +| **Stakeholders** | ITK Dev developers, data consumers, future maintainers | +| **Status** | Draft | + +## Context + +The adapter makes data available to consumers the organisation does +not control and cannot brief. The data already exists in operational systems, +with heterogeneous formats, coordinate systems and access methods. What has to +be decided is the mechanism by which it is published. The source systems remain +authoritative; the published copy is not a system of record. + +This ADR serves to decide the mechanism by which data is published to +consumers. + +### Drivers + +- **Functional:** query by location and attribute, not bulk download only; + several data sets reaching one consumer-facing surface; change notification; + adding a data set without changing consumer integrations. +- **Non-functional:** interpretable by consumers we have never spoken to; + operational cost proportionate to the data and the number of consumers; + existing client tooling rather than clients we supply; an interface that + outlives individual data sets. + +### Options Considered + +1. **An NGSI-LD context broker.** Provides geospatial and attribute queries, + pagination, subscriptions and a temporal interface without implementing + them; payloads carry a vocabulary reference, so they are self-describing; + many producers converge on one consumer surface. Substantial operational + weight — typically a database and message bus alongside the broker — and the + strictness of a particular implementation is inherited. +2. **Static file export** on a web server or object store. Near-zero + operational cost, trivially cacheable, readable by anything. No query, so + consumers download everything and filter client-side; no change + notification; conventions must be documented in prose because nothing in the + file declares its own meaning. +3. **A bespoke REST API** over our own datastore. Exact fit, full control of + the query surface and semantics. Every capability is ours to build and + maintain — geo-queries, filtering, pagination, notifications, documentation, + clients, versioning — and consumers must learn an interface that exists + nowhere else. +4. **Direct database access or a read replica.** No API layer, powerful ad-hoc + querying. Exposes internal schema as a public contract, requires per-consumer + credentials and network access, and is unusable by browser-based consumers. + +## Decision + +Publish to an **NGSI-LD context broker**. + +- Consumers of geographic data need "everything within this area" and + "everything of this kind" more often than the whole data set. A broker + provides that as a standard interface rather than a per-data-set feature. +- Publishing structure without a vocabulary reference requires every consumer + to have our documentation. A broker payload carries the reference. +- For a single small data set a static export would be cheaper and better. Once + several heterogeneous data sets must be published, the fixed operational cost + is paid once while the per-data-set cost approaches zero, and consumers + integrate once rather than once per source. +- New consumers require no change to the adapter, and new data sets require no + change to consumers. +- Existing viewers, dashboards and connectors speak this interface; a bespoke + API would mean supplying clients indefinitely. + +The broker's value is interoperability and query, not storage. If no consumer +reads the data through its interface, a static export would have been the better +decision. Revisit once data sets have been published long enough for consumers +to appear. + +## Consequences + +### Positive + +- Geospatial and attribute queries, pagination, subscriptions and a temporal + interface, none of which are implemented here. +- Payloads reference a shared vocabulary, so they need no bespoke + documentation. +- Additional data sets reach every existing consumer with no integration work. +- The adapter has no database and no read surface of its own. + +### Negative / Trade-offs + +- **Operational weight out of proportion to a small data set.** A broker + deployment is several services to run, patch, monitor and back up. For a + static data set a file on a web server would serve the same need. +- **Broker implementations impose constraints beyond the standard.** Those + encountered include accepting only one spelling of a UTC timestamp while + rejecting an equivalent one, requiring the vocabulary reference on read + requests — with omission returning an empty success rather than an error — + and collapsing single-element lists to scalars. +- **Vocabulary documents may be fetched over the network during writes**, so + third-party availability becomes part of the import path. +- **No delete semantics.** Upsert creates and updates but never removes, so + records that disappear upstream persist until reconciliation is built. +- JSON-LD is a learning curve for maintainers and consumers. +- Fitting data to a shared vocabulary costs effort that publishing as-is + would not. diff --git a/docs/adr/003-ngsi-ld-representation.md b/docs/adr/003-ngsi-ld-representation.md new file mode 100644 index 0000000..8adcb7e --- /dev/null +++ b/docs/adr/003-ngsi-ld-representation.md @@ -0,0 +1,70 @@ +# 003: NGSI-LD representation — normalized form and batch upsert + +| Field | Value | +|--------------------|--------------------------------------------------------| +| **Created By** | Jeppe Krogh | +| **Date** | 2026-08-24 | +| **Decision Maker** | ITK Dev team | +| **Stakeholders** | ITK Dev developers, data consumers, future maintainers | +| **Status** | Draft | + +## Context + +ADR 002 chooses an NGSI-LD context broker. That leaves two representation +choices open: how attributes are shaped, and which write operation is used. + +NGSI v2, the older FIWARE API generation, was not considered viable: it has no +`@context`, so a shared vocabulary cannot be expressed, and the +organisation operates no v2 broker. + +This ADR serves to decide how attributes are shaped and which write +operation is used. + +### Drivers + +- **Functional:** the broker must accept the payload on write; attribute-level + metadata must be expressible; repeated imports must not duplicate entities. +- **Non-functional:** idempotency; payload size; readability for consumers. + +### Options Considered + +#### Attribute form + +1. **Normalized** — every attribute an object naming its kind. Verbose, but + the form brokers accept on write and the only one carrying metadata such as + `observedAt`. +2. **Key-values** — flat `name: value`. Much smaller and easier to read, but + read-only and cannot carry attribute metadata. + +#### Write operation + +1. **Batch upsert** — creates or updates. Idempotent when identifiers are + derived from source keys. +2. **Create** — fails with `409` for identifiers that already exist, so a + re-import errors rather than refreshing. +3. **Batch replace** — silently drops attributes absent from the payload, + making partial payloads destructive. + +## Decision + +Publish **normalized** NGSI-LD with `Content-Type: application/ld+json`, via +**batch upsert**. + +- Normalized is the only form accepted on write, so key-values is not a real + option for a producer. +- Upsert makes imports idempotent: identifiers derive from each source's + primary key, so re-running updates in place. + +## Consequences + +### Positive + +- Re-imports produce no duplicates and need no prior state. +- Attribute metadata remains available if a source ever supplies it. + +### Negative / Trade-offs + +- Payloads are considerably larger than key-values. +- Upsert never deletes, so records removed upstream persist until + reconciliation is built (see ADR 002). +- Consumers unfamiliar with JSON-LD face a learning curve. diff --git a/docs/adr/004-coordinate-reference-system.md b/docs/adr/004-coordinate-reference-system.md new file mode 100644 index 0000000..177d6b7 --- /dev/null +++ b/docs/adr/004-coordinate-reference-system.md @@ -0,0 +1,112 @@ +# 004: Coordinate reference system — publish WGS84 (EPSG:4326) + +| Field | Value | +|--------------------|----------------------------------------------------------| +| **Created By** | Jeppe Krogh | +| **Date** | 2026-08-27 | +| **Decision Maker** | ITK Dev team | +| **Stakeholders** | ITK Dev developers, broker consumers, future maintainers | +| **Status** | Draft | + +## Context + +Every entity the adapter publishes carries a `location` GeoProperty, so the +coordinate reference system is a cross-cutting concern rather than a per-source +detail. + +Input data arrives in whatever CRS its publisher uses. Danish municipal data is +commonly projected — typically EPSG:25832 (ETRS89 / UTM zone 32N), as eastings +and northings in metres. Other inputs may already be geographic, or use a +different projection. The adapter cannot assume one input CRS. + +Projected coordinates are sometimes delivered inside a GeoJSON envelope, which +states a geometry type but not units. The data models specify only that +`location` is GeoJSON and make no reference to a coordinate system; the +constraint comes from GeoJSON itself. + +This ADR serves to decide which coordinate reference system is published, +and at what precision. + +### Drivers + +- **Functional:** consumers must be able to interpret `location` unbriefed; + geo-queries must return correct results; clients must render without + preprocessing; one rule must hold for every input. +- **Non-functional:** self-description; conformance; uniformity across inputs; + precision no worse than the input. + +### Options Considered + +1. **Normalise everything to WGS84, reprojecting in the adapter.** Conforms to + RFC 7946; self-describing; geo-queries work; one rule however many input + CRSs accumulate. Requires a reprojection dependency, and each input must + declare its CRS. +2. **Pass each input's native CRS through unchanged.** No transformation, no + dependency — but produces invalid GeoJSON with nowhere to declare the CRS, + makes entities from different inputs mutually incomparable, and breaks + geo-queries because distances are read as degrees. Every failure is silent. +3. **Publish WGS84 and also retain original coordinates in an extra + attribute.** Avoids a round trip for consumers wanting native coordinates, + but the attribute cannot have a stable shape: each input brings its own CRS + and geometry type, and it would be absent for inputs already in WGS84. A + consumer cannot code against that, so it would go unused. +4. **Pass native CRSs through under RFC 7946's "prior arrangement" clause, + documenting each out of band.** Permitted by the RFC, but the clause + requires all parties to have agreed — incompatible with a broker whose + consumers are unknown by design. + +## Decision + +Every `location` the adapter emits is **EPSG:4326 (WGS84) +longitude/latitude**, at **full precision — coordinates are not rounded**. Each +input declares its own CRS; reprojection happens at the boundary between reading +an input and building an entity, and nowhere else. + +- **The specification is unambiguous.** RFC 7946 §4: "The coordinate reference + system for all GeoJSON coordinates is a geographic coordinate reference + system, using the World Geodetic System 1984 (WGS 84) datum, with longitude + and latitude units of decimal degrees." NGSI-LD GeoProperty values are + GeoJSON, so the requirement is inherited. +- **There is no way to declare otherwise.** RFC 7946 Appendix B.1: + "Specification of coordinate reference systems has been removed, i.e., the + 'crs' member of [GJ2008] is no longer used." Publishing projected coordinates + means publishing an undeclarable assumption. Inputs may still carry that + deprecated member; it can be read, but not passed on. +- Entities from different inputs are queried together, so a query spanning two + inputs published in different CRSs returns meaningless results. +- A broker given projected coordinates accepts them, answers geo-queries + incorrectly, and renders points in the wrong location. No error is raised at + any stage. +- **The conversion is lossless at full precision.** A projected-to-geographic + round trip returns the input exactly when no rounding is applied. Rounding + trades accuracy for a marginal reduction in payload size. +- `source` and `seeAlso` can reference the originating export, which states its + own CRS. A coordinate copied into an extra attribute states nothing. + +## Consequences + +### Positive + +- Payloads are valid GeoJSON and NGSI-LD; geo-queries work and are comparable + across inputs; any client renders them unmodified. +- One rule for every present and future input. +- Reprojection is isolated in one component with its own tests, verified + against independently known reference coordinates, so a regression fails + loudly instead of silently relocating data. + +### Negative / Trade-offs + +- Adds a reprojection dependency. National grid definitions are not always + shipped and may need registering explicitly, making them load-bearing + project code. +- **Datum shifts are approximated.** ETRS89-based grids are treated as + equivalent to WGS84 via a null datum transformation. The two were coincident + in 1989 and have diverged by roughly 0.5–1 m since, at about 2.5 cm per year. + What is published is therefore ETRS89 labelled WGS84. This is standard + practice in web GIS, but it is the largest error in the pipeline — greater + than the source's own positional accuracy — so a consumer using a rigorous + transformation with an explicit epoch will land about a metre away. +- Consumers with natively projected stacks must convert. +- Every new input must declare its CRS, and unsupported ones need adding. +- Only point geometries were implemented initially; line and area geometries + were added later. diff --git a/docs/adr/005-smart-data-models-as-vocabulary.md b/docs/adr/005-smart-data-models-as-vocabulary.md new file mode 100644 index 0000000..42f11ec --- /dev/null +++ b/docs/adr/005-smart-data-models-as-vocabulary.md @@ -0,0 +1,126 @@ +# 005: Vocabulary — adopt Smart Data Models + +| Field | Value | +|--------------------|--------------------------------------------------------| +| **Created By** | Jeppe Krogh | +| **Date** | 2026-08-31 | +| **Decision Maker** | ITK Dev team | +| **Stakeholders** | ITK Dev developers, data consumers, future maintainers | +| **Status** | Draft | + +## Context + +NGSI-LD defines how attributes are carried and how to reference a vocabulary. +It does not define entity types or attribute names. Without a vocabulary the +JSON-LD context resolves to nothing, entity types are local strings, and +consumers still need our documentation to interpret anything. + +The choice also determines the cost of onboarding each data set, since mapping +a source onto an existing model takes more effort than exposing its fields +verbatim. + +This ADR serves to decide which vocabulary supplies entity types and +attribute names, and the rules for mapping sources onto it. + +### Drivers + +- **Functional:** types and attributes interpretable without our + documentation; expressible as a JSON-LD context a broker can resolve; + coverage across the domains in scope. +- **Non-functional:** a vocabulary consumers plausibly already know; governed + and maintained by someone else; mapping cost that does not dominate + onboarding. + +### Options Considered + +1. **Smart Data Models.** Purpose-built for NGSI-LD and the reference + vocabulary of that ecosystem; publishes JSON-LD context documents per + domain; broad coverage; each model ships a JSON schema and examples, giving + an objective conformance target; open governance. Model depth varies; + many models assume real-time sensing, so static inventory leaves attributes + unset; required attributes occasionally presuppose a hierarchy the source + lacks; enum spellings sometimes disagree between a model's schema and its + examples; versioning is loose. +2. **A vocabulary of our own, with self-hosted context documents.** Exact fit, + no required attributes we cannot satisfy, full control of naming and + versioning. Nobody else speaks it, so consumers return to reading our + documentation; governance, documentation and versioning become ours + indefinitely; no existing tooling recognises the types. +3. **A general-purpose web vocabulary.** Widely recognised, stable governance, + adequate for names, addresses and descriptions. No NGSI-LD conventions for + geometry or relationships, and no domain-specific terms, so the domains in + scope would remain unmodelled. + +## Decision + +Adopt **Smart Data Models**, referencing the relevant domain context documents +alongside the NGSI-LD core context. + +Two rules follow, and they matter more than the choice itself: + +1. **Use an existing model; do not invent a type.** An imperfect standard type + is more useful to a consumer than a perfect private one. +2. **Never fabricate a value to satisfy a model.** An absent attribute is + honest; a fabricated one is indistinguishable from a measured one. Applied: + + - Attributes the source cannot fill are left unset, not approximated, + defaulted or inferred. + - Where a model *requires* an attribute the source cannot supply, choose a + different model rather than inventing the value. Required relationships + are the common case: inventing a related entity yields something + schema-valid and factually wrong that must then be maintained + indefinitely. A sibling model without the requirement is the better + choice even if its terms are less precise. + - In a hierarchy — a root site, subdivisions beneath it, individual units + beneath those, each lower level requiring a relationship upward — publish + at the highest level the source can populate. Static inventory typically + describes a location and a count of units without describing what the + location is part of, so the site level is usually correct. + +The concrete model chosen for a given data set is recorded in its own ADR; this +one states policy only. + +Rationale: + +- The context must resolve to terms a consumer recognises, or publishing gains + nothing over a file. +- Smart Data Models is the vocabulary the surrounding ecosystem uses and ships + the context documents needed to reference it, so adoption is a URL rather + than a project. +- Shipped schemas and examples make modelling disagreements checkable against a + specification instead of settled by preference. +- Mandatory relationships propagate downward: choosing a subdivision level + schedules the need for a parent rather than avoiding it, because the + individual-unit level requires a site as well. +- The costs are asymmetric. Publishing at site level and later finding real + sites exist means a one-off migration. Publishing at subdivision level and + never acquiring real sites means maintaining an invented entity + indefinitely, with every consumer that follows the relationship receiving + something meaningless. + +## Consequences + +### Positive + +- Types and attributes resolve to shared global identifiers. +- Consumers may already have code for the types published. +- Modelling decisions have an external reference point. +- Later data sets are likely already covered, so onboarding does not start with + vocabulary design. +- Published entities are self-contained, with nothing invented to keep in sync. +- Finer granularity can be added later beneath what is already published. + +### Negative / Trade-offs + +- **Many attributes will always be empty.** Models built around real-time + sensing carry availability, occupancy and detection attributes that static + inventory cannot fill. +- **Model choice is embedded in entity identifiers.** Changing model later + means deleting and re-publishing rather than updating in place, so selection + deserves attention before a data set is first published. +- **Enum values must be read from the schema, not the examples.** Where the two + disagree the schema is authoritative, and equivalent-looking values differ + between sibling models, so they must not be copied across. +- **Loose versioning.** A model can change without an obvious signal. +- Mapping a source to a model takes longer than exposing its fields verbatim, + and occasionally the fit is poor. diff --git a/docs/adr/006-onstreetparking-over-parkinggroup.md b/docs/adr/006-onstreetparking-over-parkinggroup.md new file mode 100644 index 0000000..8b04272 --- /dev/null +++ b/docs/adr/006-onstreetparking-over-parkinggroup.md @@ -0,0 +1,104 @@ +# 006: Model selection — OnStreetParking over ParkingGroup + +| Field | Value | +|--------------------|--------------------------------------------------------| +| **Created By** | Jeppe Krogh | +| **Date** | 2026-08-31 | +| **Decision Maker** | ITK Dev team | +| **Stakeholders** | ITK Dev developers, data consumers, future maintainers | +| **Status** | Draft | + +## Context + +The parking domain is organised as a hierarchy: + +```text +OnStreetParking / OffStreetParking site — no parent, requires id, type, location + └── ParkingGroup subdivision — requires refParkingSite + └── ParkingSpot individual unit — requires refParkingSite, status, category +``` + +This decision applies where a data set provides a count of units per location +with a point geometry, no reference to a containing site, and neither per-unit +geometry nor occupancy. + +Entity identifiers embed the type by convention, so the choice must be made +before first publication: changing it afterwards means deleting and +re-publishing. + +This ADR serves to decide which model in the parking hierarchy is published +under those conditions. + +### Drivers + +- **Functional:** every mandatory relationship must point at an entity that + exists; the restriction on who may park must be expressible unambiguously; + finer granularity addable later without restructuring what is published. +- **Non-functional:** nothing invented purely to satisfy a schema; a choice + that is cheap to reverse in preference to one that is not. + +### Options Considered + +1. **`ParkingGroup`, creating the missing parent site.** `category` offers + `onlyDisabled`, which by name states exclusivity, and the model's reference + example for disabled parking sits at this level. But `refParkingSite` is + mandatory and no value is available for it, so a parent must be invented; + one spanning the whole administrative area asserts a false containment, and + its own mandatory geometry would carry no meaning. + `ParkingSpot` also requires a site, so adding per-unit data later would force + the invented entity into existence after entities had been published against + it. +2. **`ParkingGroup`, omitting `refParkingSite`.** Nothing invented, smallest + change — but knowingly non-conformant, and a schema validator flags every + entity. +3. **`OnStreetParking`.** Requires only `id`, `type` and `location`, all of + which are available. It is the entity both `ParkingGroup` and + `ParkingSpot` are required to reference, so finer granularity can be + attached beneath it, and migration down to `ParkingGroup` stays possible if + real site data appears. `category` offers only `forDisabled`, which does not + state exclusivity as plainly. +4. **`ParkingSpot`.** Models an individual unit, but only a count per location + is available, `status` is mandatory with no occupancy data, and a parent site + is required. Rejected outright. + +## Decision + +Publish each record as an **`OnStreetParking`** entity, with +`category: ["forDisabled"]` and no `refParkingSite`. + +- It is the only option that invents nothing; everything the model requires is + available. +- Mandatory relationships propagate downward, so choosing `ParkingGroup` would + schedule the invented parent rather than avoid it — `ParkingSpot` requires a + site too. +- The costs are asymmetric. Site level, then finding real sites exist, is a + one-off migration. Subdivision level, then never acquiring real sites, means + maintaining an invented entity indefinitely. + +The model's reference example does use `ParkingGroup` for disabled parking, but +points at a real street-address site. It shows what to do when a site exists, +not when none does. + +## Consequences + +### Positive + +- No dangling relationship; every entity is self-contained. +- Nothing invented to create, document or keep in sync. +- Conformant to the model's schema without exceptions. +- `ParkingGroup` or `ParkingSpot` entities can be attached beneath these later + without changing them. + +### Negative / Trade-offs + +- **`forDisabled` does not state exclusivity.** The schema documents `category` + only as "Street parking category" with an enum list and defines no individual + value. The two models' enums use inconsistent prefixes for what appear to be + the same concepts — `forDisabled` / `forResidents` against `onlyDisabled` / + `onlyResidents` — while both carry `onlyWithPermit`, so the spelling cannot + be relied on to carry exclusivity. +- Values must not be copied between the two models' `category` enums. +- Entity type is embedded in identifiers, so any later change means deleting + and re-publishing. +- Domain experts may find a single address described as a "site" + counter-intuitive. diff --git a/docs/adr/CLAUDE.md b/docs/adr/CLAUDE.md new file mode 100644 index 0000000..679a5cd --- /dev/null +++ b/docs/adr/CLAUDE.md @@ -0,0 +1,78 @@ +# CLAUDE.md — writing ADRs in this project + +Guidance for Claude Code when creating or editing files in `docs/adr/`. + +An ADR here **decides a specific choice on general grounds**. The choice is +concrete; the reasoning must hold beyond any one data set, developer or machine. + +## Never include + +- **Local project or repository names.** These are organisation-level documents; + someone's working copies have no place in them. +- **Specific data sources, their fields, or their quirks.** Not field names, not + envelope shapes, not "the source provides…". State the *condition* the + decision applies under instead: "This decision applies where a data set + provides a count of units per location…". +- **Measurements or counts taken from a data set.** No record counts, no + "measured across N records", no byte or centimetre figures derived from one + export. Standards identifiers (EPSG codes, RFC numbers, framework versions) + and general domain facts are fine. +- **Educational or self-referential framing.** No "two things make this easy to + get wrong", no "note that", no "the real lesson is", no bolded aphorism + followed by the actual fact, and no commentary on the ADR's own importance + ("this is the hardest to reverse"). State the fact and stop. + +## Structure + +```text +# NNN: Area — the choice + +| Field | Value | Created By, Date, Decision Maker, Stakeholders, Status + +## Context the situation; what is undecided + ends with: "This ADR serves to decide …" +### Drivers Functional / Non-functional, brief +### Options Considered one paragraph each, upsides then downsides +## Decision the choice, then terse rationale bullets +## Consequences +### Positive +### Negative / Trade-offs the honest costs, including ones found by hitting them +``` + +The Context section **must end with a single "This ADR serves to …" sentence** +so the purpose is unmissable. + +Status is `Draft` while it needs review, then `Accepted`. Other values: +`Rejected`, `Deprecated by NNN`, `Supersedes NNN`. + +## Numbering + +Numbering follows **dependency order**: every ADR may reference only +lower-numbered ones. Check for forward references after adding or renumbering. +Dates therefore need not run in the same order as numbers. + +Renumbering is only acceptable while nothing external cites the numbers. Once +cited, supersede instead. + +## Before writing, ask whether it earns its place + +- **Is there a real alternative?** An ADR comparing one viable candidate against + a dead end documents a decision that made itself, and dilutes the ones + recording genuine trade-offs. Reframe it around the decisions that did have + alternatives, or fold it into a neighbour. +- **Would folding it into an existing ADR be better?** Prefer fewer, shorter + documents. Consolidation has been chosen over adding a document more than + once. +- **Are the rules distinct?** One principle restated against several targets is + one rule with sub-points, not several rules. + +## Before finishing + +- Grep for source field names, model names in the general ADRs, project names, + record counts and measured figures. +- Grep for `Note that`, `worth noting`, `is the lesson`, `easy to get wrong`, + `Two things`, `earns its`, `is the point`. +- Check no ADR cites a higher number than its own. +- `task coding-standards:markdown:check` — recurring failures are line length + over 120 (MD013), misaligned table pipes (MD060), and bold used as a heading + (MD036); use `####` for sub-headings inside a section. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..1ce7c5e --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,20 @@ +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs) for this project. + +See [adr.github.io](https://adr.github.io/) for background on the format. + +| Number | Title | Status | Date | +| ----------------------------------------------- | ------------------------------------------------------- | -------- | ---------- | +| [001](001-architecture-symfony-docker.md) | Architecture — Symfony 8 on the ITK Dev Docker template | Accepted | 2026-08-24 | +| [002](002-publish-to-a-context-broker.md) | Publication mechanism — publish to a context broker | Accepted | 2026-08-24 | +| [003](003-ngsi-ld-representation.md) | NGSI-LD representation — normalized form, batch upsert | Accepted | 2026-08-24 | +| [004](004-coordinate-reference-system.md) | Coordinate reference system — publish WGS84 | Accepted | 2026-08-27 | +| [005](005-smart-data-models-as-vocabulary.md) | Vocabulary — adopt Smart Data Models | Accepted | 2026-08-31 | +| [006](006-onstreetparking-over-parkinggroup.md) | Model selection — OnStreetParking over ParkingGroup | Accepted | 2026-08-31 | + +Numbering follows dependency order: each ADR relies only on lower-numbered +ones. Dates therefore do not run in the same order as numbers. + +All ADRs state general policy and name no data set. Concrete per-data-set +mappings are recorded in the README and in the source classes themselves. From 9aa1a0bf7bc18375a924a3ac5473def7da1286c2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:20:49 +0200 Subject: [PATCH 007/101] Updated README --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 95afbea..4052b40 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ We use [DDEV](https://ddev.com/) and [Task](https://taskfile.dev/) for developme ``` shell task site:install +``` ``` shell task site:update @@ -12,6 +13,44 @@ ddev launch Run `task` to see what cool task are available. Running `ddev` can help with other stuff. +## Adapter + +Takes an Aarhus open-data set, converts it to [NGSI-LD], and upserts it into the +context broker. + +``` text +source feed (JSON) + → SourceInterface implementation maps fields, fixes quirks, picks the data model + → NgsiEntity normalized NGSI-LD: Property / GeoProperty / Relationship + → NgsiLdBroker POST /ngsi-ld/v1/entityOperations/upsert + → context broker +``` + +| Class | Responsibility | +| ---------------------------------- | ------------------------------------------------ | +| `App\Source\SourceInterface` | Contract for one input data set | +| `App\Source\HandicapParkingSource` | Disabled parking bays → `OnStreetParking` | +| `App\Source\FeedReader` | Path or URL → decoded JSON | +| `App\Geo\Wgs84Transformer` | Any registered CRS → WGS84, any GeoJSON geometry | +| `App\Ngsi\NgsiEntity` | Builds normalized NGSI-LD entities | +| `App\Broker\NgsiLdBroker` | Batch upsert to the broker | +| `App\Command\ImportCommand` | `app:import` | + +``` shell +task import # list the available sources +task import -- MTM-handicap-parking # import one +task import -- MTM-handicap-parking --dry-run --limit 5 # print the payload instead +task broker:entities -- OnStreetParking 10 # read back what landed +``` + +Adding a data set means adding one `SourceInterface` implementation. It is +discovered through `#[AutoconfigureTag('app.source')]` and shows up as an +`app:import` argument with no further wiring. + +Design decisions are recorded in [docs/adr](docs/adr/README.md). + +[NGSI-LD]: https://www.etsi.org/committee/cim + ## Broker A [Scorpio Broker](https://scorpio.readthedocs.io/) is part of the development setup. From 9dadf6294ec6b36b4ff2cfe03f33ad9a212dd2be Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:21:21 +0200 Subject: [PATCH 008/101] Updated CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf48d94..2e140da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `app:import` command with `--dry-run` and `--limit`, exposed as `task import`. +- `SourceInterface`: extension point for further data sets, discovered through + `#[AutoconfigureTag('app.source')]`. +- `Wgs84Transformer`: reprojects coordinates from any registered CRS to WGS84, + for a single position or a whole GeoJSON geometry of any type. +- `FeedReader`: reads a feed from a filesystem path or an http(s) URL and decodes + it, without interpreting its shape. +- `NgsiEntity`: builds normalized NGSI-LD entities. +- `NgsiLdBroker`: idempotent batch upsert to an NGSI-LD context broker. +- `task broker:entities` for reading entities back out of the broker. +- Architecture Decision Records under `docs/adr`. +- Added test suite + [Unreleased]: https://github.com/itk-dev/enter From 6de59da0dc51fca7efc660f5a69ca5811c0eae67 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:21:35 +0200 Subject: [PATCH 009/101] Added pointers to .env --- .env | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.env b/.env index 999fa0f..2dfb180 100644 --- a/.env +++ b/.env @@ -32,4 +32,18 @@ DEFAULT_URI=http://localhost ###> app ### APP_BROKER_BASE_URI=http://scorpio.local:9090/ + +# JSON-LD contexts attached to every entity, outermost last so the ETSI core +# context resolves the NGSI-LD terms and the domain context resolves the +# Smart Data Models ones. +ENTER_NGSI_CONTEXT_URLS='https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld,https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld' + +# A single domain context, for the Link header that read requests need. Only +# used by `task broker:entities`, and it must be the context defining the type +# being read — Parking while that is the only model published. +ENTER_NGSI_DOMAIN_CONTEXT=https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld + +# Where the disabled-parking export is read from: the live SpatialMap export. +# Accepts any http(s) URL, or a path relative to the project directory. +ENTER_HANDICAP_PARKING_SOURCE='https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' ###< app ### From a56f14cfc5b217fb124c44b7c458668037d75c7d Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:22:05 +0200 Subject: [PATCH 010/101] Added a few commands to Taskfile --- Taskfile.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 49f5e72..3b30260 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -36,6 +36,23 @@ tasks: test:integration: *test_task test:application: *test_task + import: + desc: 'Import a source into the broker, e.g. task import -- MTM-handicap-parking' + cmd: ddev console app:import {{.CLI_ARGS}} + + # The broker itself needs no start task: Scorpio is a DDEV custom service + # (.ddev/docker-compose.scorpio.yaml), so it comes up with the rest of the + # site. Read requests need the domain context in a Link header, which the + # script supplies; the env values come from the dotenv block above, and are + # passed in explicitly because the container shell does not read .env. + broker:entities: + desc: 'List broker entities of a type, e.g. task broker:entities -- OnStreetParking 10' + cmd: >- + ddev exec sh -c + "APP_BROKER_BASE_URI='$APP_BROKER_BASE_URI' + ENTER_NGSI_DOMAIN_CONTEXT='$ENTER_NGSI_DOMAIN_CONTEXT' + sh task/scripts/broker-entities {{.CLI_ARGS}}" + coding-standards:apply: desc: 'Apply coding standards' cmds: From ff7146dad1bca1536d92a6e47052e71146bda4d6 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:24:04 +0200 Subject: [PATCH 011/101] Added wgs84 transformer class --- src/Geo/Wgs84Transformer.php | 160 +++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/Geo/Wgs84Transformer.php diff --git a/src/Geo/Wgs84Transformer.php b/src/Geo/Wgs84Transformer.php new file mode 100644 index 0000000..9f64596 --- /dev/null +++ b/src/Geo/Wgs84Transformer.php @@ -0,0 +1,160 @@ + + */ + private const DEFINITIONS = [ + // ETRS89 / UTM zone 32N — most of Denmark. + 'EPSG:25832' => '+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + // ETRS89 / UTM zone 33N — Bornholm and eastwards. + 'EPSG:25833' => '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + // Web Mercator, in case an input arrives as tile coordinates. + 'EPSG:3857' => '+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +no_defs', + self::TARGET_SRID => '+proj=longlat +datum=WGS84 +no_defs', + ]; + + private readonly Proj4php $proj4; + + /** @var array */ + private readonly array $definitions; + + /** @var array */ + private array $projections = []; + + /** + * @param array $definitions additional PROJ definitions, keyed by SRID + */ + public function __construct(array $definitions = []) + { + $this->definitions = [...self::DEFINITIONS, ...$definitions]; + + $this->proj4 = new Proj4php(); + foreach ($this->definitions as $srid => $definition) { + $this->proj4->addDef($srid, $definition); + } + } + + /** + * Coordinates are not rounded. Downstream use is unknown and may include + * planning work, so the transformed value is published as computed. + * + * @param string $srid source CRS, e.g. "EPSG:25832" + * + * @return array{float, float} GeoJSON coordinate order: [longitude, latitude] + */ + public function toWgs84(string $srid, float $x, float $y): array + { + if (self::TARGET_SRID === $srid) { + return [$x, $y]; + } + + $transformed = $this->proj4->transform( + $this->projection(self::TARGET_SRID), + new Point($x, $y, $this->projection($srid)) + ); + + return [(float) $transformed->x, (float) $transformed->y]; + } + + /** + * @param string $srid source CRS, e.g. "EPSG:25832" + * + * @return array{type: string, coordinates: array{float, float}} GeoJSON Point + */ + public function point(string $srid, float $x, float $y): array + { + return [ + 'type' => 'Point', + 'coordinates' => $this->toWgs84($srid, $x, $y), + ]; + } + + /** + * Reprojects a whole GeoJSON geometry, whatever its type. + * + * Handles Point, LineString, Polygon, MultiPoint, MultiLineString and + * MultiPolygon — every type an NGSI-LD GeoProperty accepts. GeometryCollection + * is not supported, because it carries `geometries` rather than `coordinates`. + * + * A third ordinate (elevation) is dropped: the inputs are two-dimensional, + * and vertical datums are a separate concern this class does not model. + * + * @param string $srid source CRS, e.g. "EPSG:25832" + * @param array $geometry GeoJSON geometry object + * + * @return array{type: string, coordinates: mixed} + */ + public function geometry(string $srid, array $geometry): array + { + $type = $geometry['type'] ?? null; + + if (!\is_string($type) || !\array_key_exists('coordinates', $geometry)) { + throw new \InvalidArgumentException('Not a GeoJSON geometry: "type" and "coordinates" are both required.'); + } + + return [ + 'type' => $type, + 'coordinates' => $this->transformCoordinates($srid, $geometry['coordinates']), + ]; + } + + /** + * GeoJSON nests coordinates to a depth that depends on the geometry type: + * a bare position for Point, an array of positions for LineString, an array + * of those for Polygon, and so on. Recursing until the first element is + * numeric handles every depth without enumerating the types. + * + * @return array + */ + private function transformCoordinates(string $srid, mixed $coordinates): array + { + if (!\is_array($coordinates) || [] === $coordinates) { + throw new \InvalidArgumentException('GeoJSON coordinates must be a non-empty array.'); + } + + if (is_numeric($coordinates[0] ?? null)) { + if (!is_numeric($coordinates[1] ?? null)) { + throw new \InvalidArgumentException('A GeoJSON position needs at least two ordinates.'); + } + + return $this->toWgs84($srid, (float) $coordinates[0], (float) $coordinates[1]); + } + + return array_map( + fn (mixed $nested): array => $this->transformCoordinates($srid, $nested), + array_values($coordinates) + ); + } + + private function projection(string $srid): Proj + { + if (!isset($this->definitions[$srid])) { + throw new \InvalidArgumentException(\sprintf('Unknown CRS "%s". Register a PROJ definition for it before use. Known: %s.', $srid, implode(', ', array_keys($this->definitions)))); + } + + return $this->projections[$srid] ??= new Proj($srid, $this->proj4); + } +} From 0386a7d2fb69484464b91a81725b4a89b7c0cdac Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:24:38 +0200 Subject: [PATCH 012/101] Added a generic way to retrieve data from broker via terminal --- task/scripts/broker-entities | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 task/scripts/broker-entities diff --git a/task/scripts/broker-entities b/task/scripts/broker-entities new file mode 100755 index 0000000..280bb6c --- /dev/null +++ b/task/scripts/broker-entities @@ -0,0 +1,34 @@ +#!/bin/sh +# List entities of a given type from the NGSI-LD broker. +# +# Read requests need a domain context supplied in a Link header; without it the +# broker cannot expand a short type name into the full term the entity was +# written under. The context comes from ENTER_NGSI_DOMAIN_CONTEXT, which must +# be the one defining the type being asked for. +# +# Usage: broker-entities [limit] + +set -eu + +if [ $# -lt 1 ]; then + echo "Usage: broker-entities [limit]" >&2 + echo " NGSI-LD entity type, e.g. OnStreetParking" >&2 + echo " [limit] maximum entities to return (default 100)" >&2 + exit 64 +fi + +TYPE="$1" +LIMIT="${2:-100}" + +RESPONSE=$(curl -sS \ + -H "Link: <${ENTER_NGSI_DOMAIN_CONTEXT}>; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\"" \ + "${APP_BROKER_BASE_URI%/}/ngsi-ld/v1/entities?type=${TYPE}&limit=${LIMIT}") + +printf '%s\n' "$RESPONSE" + +# A misspelled type, or one whose context is not the one configured, returns an +# empty list with HTTP 200 — indistinguishable from an empty broker or a failed +# import. Say what was asked for so the difference is visible. +if [ "$(printf '%s' "$RESPONSE" | tr -d '[:space:]')" = "[]" ]; then + printf '\nNo entities of type "%s". A type that does not match, or one defined in a context other than the configured ENTER_NGSI_DOMAIN_CONTEXT, returns an empty list rather than an error — check both before concluding the import failed.\n' "$TYPE" >&2 +fi From 492d105e055f1494d7daa1a356b82600aaaa1a9a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:32:57 +0200 Subject: [PATCH 013/101] Added feedReader --- src/Source/FeedReader.php | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/Source/FeedReader.php diff --git a/src/Source/FeedReader.php b/src/Source/FeedReader.php new file mode 100644 index 0000000..6f8677c --- /dev/null +++ b/src/Source/FeedReader.php @@ -0,0 +1,58 @@ + the decoded document + * + * @throws \RuntimeException when the location is not an http(s) URL, cannot + * be fetched, does not contain valid JSON, or + * does not decode to an array + */ + public function read(string $location): array + { + if (!str_starts_with($location, 'http://') && !str_starts_with($location, 'https://')) { + throw new \RuntimeException(\sprintf('Feed location must be an http(s) URL, got "%s".', $location)); + } + + $json = $this->fetch($location); + + try { + $decoded = json_decode($json, true, 512, \JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new \RuntimeException(\sprintf('Invalid JSON in "%s": %s', $location, $exception->getMessage()), previous: $exception); + } + + // A JSON document may legally be a scalar. Every feed we consume is a + // list or an object, and a scalar here means the location is wrong + // rather than that the feed is empty. + if (!\is_array($decoded)) { + throw new \RuntimeException(\sprintf('Expected a JSON array or object in "%s", got %s.', $location, get_debug_type($decoded))); + } + + return $decoded; + } + + private function fetch(string $url): string + { + try { + return $this->client->request('GET', $url)->getContent(); + } catch (\Throwable $exception) { + throw new \RuntimeException(\sprintf('Could not fetch "%s": %s', $url, $exception->getMessage()), previous: $exception); + } + } +} From 6a2ebea1610ae03478860810ac0d88babe61954a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:43:57 +0200 Subject: [PATCH 014/101] Added import command --- src/Command/ImportCommand.php | 158 ++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/Command/ImportCommand.php diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php new file mode 100644 index 0000000..1dab064 --- /dev/null +++ b/src/Command/ImportCommand.php @@ -0,0 +1,158 @@ + $sources + */ + public function __construct( + #[AutowireIterator('app.source')] + private readonly iterable $sources, + private readonly NgsiLdBroker $broker, + #[Autowire(env: 'ENTER_NGSI_CONTEXT_URLS')] + private readonly string $contextUrls, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('source', InputArgument::OPTIONAL, 'Source to import. Omit to list the available sources.') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Print the NGSI-LD payload instead of sending it.') + ->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Import at most this many entities.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $sources = []; + foreach ($this->sources as $source) { + $sources[$source->key()] = $source; + } + + if ([] === $sources) { + $io->error('No data sources are registered.'); + $io->listing([ + 'A source must implement App\Source\SourceInterface.', + 'Implementations are picked up automatically — check the class exists and is not excluded from the container.', + ]); + + return Command::FAILURE; + } + + $key = $input->getArgument('source'); + + if (null === $key) { + // Options only make sense together with a source. Listing the + // sources and exiting successfully would look like an import ran. + if ($input->getOption('dry-run') || null !== $input->getOption('limit')) { + $io->error(\sprintf( + 'No source given. Available: %s.', + implode(', ', array_keys($sources)) + )); + + return Command::INVALID; + } + + $io->section('Available sources'); + $io->listing(array_keys($sources)); + + return Command::SUCCESS; + } + + if (!isset($sources[$key])) { + $io->error(\sprintf('Unknown source "%s". Available: %s.', $key, implode(', ', array_keys($sources)))); + + return Command::INVALID; + } + + $limit = null !== $input->getOption('limit') ? max(1, (int) $input->getOption('limit')) : null; + $contexts = $this->contexts(); + + $payload = []; + foreach ($sources[$key]->entities() as $entity) { + $payload[] = $entity->toArray($contexts); + + if (null !== $limit && \count($payload) >= $limit) { + break; + } + } + + // A source that yields nothing is almost always misconfigured rather + // than genuinely empty, and it fails silently by construction: a + // record skipped for a missing field looks exactly like a feed with no + // records. Fail loudly so it cannot be mistaken for a successful run. + if ([] === $payload) { + $io->error(\sprintf('Source "%s" produced no entities.', $key)); + $io->text( + 'The source ran to completion without raising an exception, so every record was ' + .'discarded by the source\'s own guards rather than failing. Verbosity flags will ' + .'not reveal more: there is no exception to show.' + ); + $io->listing([ + 'Does the configured path or URL point at the intended document?', + 'Does the document match the shape the source expects — envelope, nesting, field names?', + 'Which guard returns early — a missing identifier, or a missing geometry?', + ]); + + return Command::FAILURE; + } + + if ($input->getOption('dry-run')) { + $output->writeln(json_encode($payload, self::JSON_FLAGS)); + $io->note(\sprintf('Dry run: %d entities were not sent.', \count($payload))); + + return Command::SUCCESS; + } + + try { + $status = $this->broker->upsert($payload); + } catch (\Throwable $exception) { + $io->error($exception->getMessage()); + + return Command::FAILURE; + } + + $io->success(\sprintf( + 'Upserted %d entities into %s (HTTP %d).', + \count($payload), + $this->broker->brokerUrl(), + $status + )); + + return Command::SUCCESS; + } + + /** + * @return list + */ + private function contexts(): array + { + return array_values(array_filter(array_map('trim', explode(',', $this->contextUrls)))); + } +} From 1fdbf473fd67ef6e02ad9dd7988ae3f572aaf0a6 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 13:46:27 +0200 Subject: [PATCH 015/101] Added source interface for picking up new source adapters --- src/Source/SourceInterface.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/Source/SourceInterface.php diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php new file mode 100644 index 0000000..978c5ce --- /dev/null +++ b/src/Source/SourceInterface.php @@ -0,0 +1,30 @@ + + */ + public function entities(): iterable; +} From d518d7540e35f341e09804d366430a28878a3595 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 14:00:51 +0200 Subject: [PATCH 016/101] Added mtm spatialmaps handicap parking source adapter --- src/Source/MtmSpatialMaps/HandicapParking.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Source/MtmSpatialMaps/HandicapParking.php diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php new file mode 100644 index 0000000..62414b5 --- /dev/null +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -0,0 +1,106 @@ +reader->read($this->location)['features'] ?? [] as $feature) { + if (\is_array($feature) && null !== $entity = $this->toEntity($feature)) { + yield $entity; + } + } + } + + /** + * @param array $feature GeoJSON Feature + */ + private function toEntity(array $feature): ?NgsiEntity + { + // A Feature keeps its attributes under `properties` and its geometry + // beside them, so neither is at the feature's top level. + $row = $feature['properties'] ?? null; + $geometry = $feature['geometry'] ?? null; + + if (!\is_array($row) || !\is_array($geometry)) { + return null; + } + + // mi_prinx is the feed's stable primary key. Without it there is no + // way to address the same bay again on the next import, and an upsert + // would create duplicates instead of updating. + $key = $row['mi_prinx'] ?? null; + if (null === $key || '' === $key) { + return null; + } + + $entity = new NgsiEntity( + \sprintf('urn:ngsi-ld:OnStreetParking:aarhus-handicap-%s', $key), + 'OnStreetParking' + ); + + // `forDisabled` rather than ParkingGroup's `onlyDisabled`: the two + // models have separate category enums, so values are not interchangeable. + // `onStreet` is dropped because the entity type already states it. + return $entity + ->property('name', $this->address($row)) + ->property('description', trim((string) ($row['bemrk'] ?? ''))) + ->property('category', ['forDisabled']) + ->property('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) + ->property('source', $this->location) + ->geoProperty('location', $this->transformer->geometry(self::SOURCE_SRID, $geometry)); + } + + /** + * @param array $row + */ + private function address(array $row): string + { + return trim(\sprintf( + '%s %s', + trim((string) ($row['vejnavn'] ?? '')), + trim((string) ($row['husnnr'] ?? '')) + )); + } +} From 1cde88ac7f06b6d281c35e69630fe176ce396d1f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 14:04:10 +0200 Subject: [PATCH 017/101] Added NgsiEntity for normalized NGSI-LD output --- src/Ngsi/NgsiEntity.php | 81 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/Ngsi/NgsiEntity.php diff --git a/src/Ngsi/NgsiEntity.php b/src/Ngsi/NgsiEntity.php new file mode 100644 index 0000000..6f38391 --- /dev/null +++ b/src/Ngsi/NgsiEntity.php @@ -0,0 +1,81 @@ +> */ + private array $attributes = []; + + public function __construct( + private readonly string $id, + private readonly string $type, + ) { + } + + public function id(): string + { + return $this->id; + } + + /** + * Null and empty-string values are dropped rather than emitted as null, + * because the source data uses "" for "not filled in" and a broker would + * otherwise store the emptiness as a fact. + */ + public function property(string $name, mixed $value, ?string $observedAt = null): self + { + if (null === $value || '' === $value || [] === $value) { + return $this; + } + + $attribute = ['type' => 'Property', 'value' => $value]; + + if (null !== $observedAt) { + $attribute['observedAt'] = $observedAt; + } + + $this->attributes[$name] = $attribute; + + return $this; + } + + /** + * @param array{type: string, coordinates: mixed} $geoJson + */ + public function geoProperty(string $name, array $geoJson): self + { + $this->attributes[$name] = ['type' => 'GeoProperty', 'value' => $geoJson]; + + return $this; + } + + public function relationship(string $name, string $object): self + { + $this->attributes[$name] = ['type' => 'Relationship', 'object' => $object]; + + return $this; + } + + /** + * @param list $contextUrls + * + * @return array + */ + public function toArray(array $contextUrls): array + { + return [ + 'id' => $this->id, + 'type' => $this->type, + ...$this->attributes, + '@context' => $contextUrls, + ]; + } +} From 20798f263aed751a8b93af7bfd3861ef1f8c5749 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 14:05:21 +0200 Subject: [PATCH 018/101] Added NgsiLdBroker for idempotent batch upserts --- src/Broker/NgsiLdBroker.php | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/Broker/NgsiLdBroker.php diff --git a/src/Broker/NgsiLdBroker.php b/src/Broker/NgsiLdBroker.php new file mode 100644 index 0000000..2ce7651 --- /dev/null +++ b/src/Broker/NgsiLdBroker.php @@ -0,0 +1,69 @@ +> $entities + * + * @return int the broker's HTTP status code + */ + public function upsert(array $entities): int + { + if ([] === $entities) { + return 204; + } + + $response = $this->client->request( + 'POST', + rtrim($this->brokerUrl, '/').self::UPSERT_PATH, + [ + 'headers' => ['Content-Type' => self::CONTENT_TYPE], + 'json' => $entities, + ] + ); + + $status = $response->getStatusCode(); + + if ($status >= 400) { + throw new \RuntimeException(\sprintf('Broker rejected the upsert with HTTP %d: %s', $status, $response->getContent(false))); + } + + return $status; + } + + public function brokerUrl(): string + { + return $this->brokerUrl; + } +} From 61f6190d154f4ddbf243d83f86c7d0287f12a53f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 14:05:33 +0200 Subject: [PATCH 019/101] Added tests --- tests/Command/ImportCommandTest.php | 148 ++++++++++++++ tests/Geo/Wgs84TransformerTest.php | 182 ++++++++++++++++++ tests/Source/FeedReaderTest.php | 114 +++++++++++ .../MtmSpatialMaps/HandicapParkingTest.php | 129 +++++++++++++ 4 files changed, 573 insertions(+) create mode 100644 tests/Command/ImportCommandTest.php create mode 100644 tests/Geo/Wgs84TransformerTest.php create mode 100644 tests/Source/FeedReaderTest.php create mode 100644 tests/Source/MtmSpatialMaps/HandicapParkingTest.php diff --git a/tests/Command/ImportCommandTest.php b/tests/Command/ImportCommandTest.php new file mode 100644 index 0000000..23830aa --- /dev/null +++ b/tests/Command/ImportCommandTest.php @@ -0,0 +1,148 @@ + $sources + */ + private function tester(iterable $sources): CommandTester + { + return new CommandTester(new ImportCommand( + $sources, + new NgsiLdBroker(new MockHttpClient(), 'http://broker.invalid'), + 'https://example.com/context.jsonld', + )); + } + + private function source(string $key, NgsiEntity ...$entities): SourceInterface + { + return new class($key, $entities) implements SourceInterface { + /** @param list $entities */ + public function __construct( + private readonly string $key, + private readonly array $entities, + ) { + } + + public function key(): string + { + return $this->key; + } + + public function entities(): iterable + { + yield from $this->entities; + } + }; + } + + /** + * The important one: a source yielding nothing used to exit successfully + * with a warning, which is indistinguishable from a working import. + */ + public function testItFailsWhenASourceProducesNothing(): void + { + $tester = $this->tester([$this->source('empty-source')]); + + $status = $tester->execute(['source' => 'empty-source', '--dry-run' => true]); + + $this->assertSame(Command::FAILURE, $status); + $this->assertStringContainsString('produced no entities', $tester->getDisplay()); + } + + public function testItSuggestsCausesWhenASourceProducesNothing(): void + { + $tester = $this->tester([$this->source('empty-source')]); + $tester->execute(['source' => 'empty-source', '--dry-run' => true]); + + $display = $tester->getDisplay(); + + $this->assertStringContainsString('path or URL', $display); + $this->assertStringContainsString('envelope, nesting, field names', $display); + } + + public function testItFailsWhenNoSourcesAreRegistered(): void + { + $tester = $this->tester([]); + + $status = $tester->execute([]); + + $this->assertSame(Command::FAILURE, $status); + $this->assertStringContainsString('No data sources are registered', $tester->getDisplay()); + } + + /** + * Passing --dry-run with no source used to print the source listing and + * exit successfully, silently ignoring the flag. + */ + public function testItRejectsOptionsWithoutASource(): void + { + $tester = $this->tester([$this->source('some-source')]); + + $status = $tester->execute(['--dry-run' => true]); + + $this->assertSame(Command::INVALID, $status); + $this->assertStringContainsString('No source given', $tester->getDisplay()); + } + + public function testItStillListsSourcesWhenCalledBare(): void + { + $tester = $this->tester([$this->source('some-source')]); + + $status = $tester->execute([]); + + $this->assertSame(Command::SUCCESS, $status); + $this->assertStringContainsString('some-source', $tester->getDisplay()); + } + + public function testItRejectsAnUnknownSource(): void + { + $tester = $this->tester([$this->source('some-source')]); + + $status = $tester->execute(['source' => 'nope']); + + $this->assertSame(Command::INVALID, $status); + $this->assertStringContainsString('Unknown source "nope"', $tester->getDisplay()); + } + + public function testDryRunPrintsThePayloadAndSendsNothing(): void + { + $entity = (new NgsiEntity('urn:ngsi-ld:Example:1', 'Example')) + ->property('name', 'Example'); + + $tester = $this->tester([$this->source('one-entity', $entity)]); + + $status = $tester->execute(['source' => 'one-entity', '--dry-run' => true]); + $display = $tester->getDisplay(); + + $this->assertSame(Command::SUCCESS, $status); + $this->assertStringContainsString('"urn:ngsi-ld:Example:1"', $display); + $this->assertStringContainsString('1 entities were not sent', $display); + } + + public function testLimitCapsThePayload(): void + { + $entities = []; + foreach (range(1, 5) as $i) { + $entities[] = new NgsiEntity(\sprintf('urn:ngsi-ld:Example:%d', $i), 'Example'); + } + + $tester = $this->tester([$this->source('many', ...$entities)]); + $tester->execute(['source' => 'many', '--dry-run' => true, '--limit' => 2]); + + $this->assertStringContainsString('2 entities were not sent', $tester->getDisplay()); + } +} diff --git a/tests/Geo/Wgs84TransformerTest.php b/tests/Geo/Wgs84TransformerTest.php new file mode 100644 index 0000000..7e3d823 --- /dev/null +++ b/tests/Geo/Wgs84TransformerTest.php @@ -0,0 +1,182 @@ +transformer = new Wgs84Transformer(); + } + + public function testItReprojectsFromUtm32ToWgs84(): void + { + [$longitude, $latitude] = $this->transformer->toWgs84( + self::UTM32, + self::REFERENCE_EASTING, + self::REFERENCE_NORTHING + ); + + $this->assertEqualsWithDelta(10.2152, $longitude, 0.001); + $this->assertEqualsWithDelta(56.1592, $latitude, 0.001); + } + + public function testItReturnsLongitudeLatitudeOrder(): void + { + $point = $this->transformer->point(self::UTM32, 574108.2557507273, 6222343.6199512165); + + $this->assertSame('Point', $point['type']); + + // Longitude first, per GeoJSON. A swapped pair is easy to spot here: + // latitude cannot be 10.19. + $this->assertGreaterThan(9.0, $point['coordinates'][0]); + $this->assertLessThan(11.0, $point['coordinates'][0]); + $this->assertGreaterThan(55.0, $point['coordinates'][1]); + $this->assertLessThan(57.0, $point['coordinates'][1]); + } + + public function testItPassesThroughCoordinatesAlreadyInWgs84(): void + { + $this->assertSame( + [10.199512, 56.149753], + $this->transformer->toWgs84(Wgs84Transformer::TARGET_SRID, 10.199512, 56.149753) + ); + } + + public function testItSupportsAnotherRegisteredCrs(): void + { + // UTM zone 33N: same northing, easting near the zone's central + // meridian, so the result must land east of zone 32's output. + [$longitude] = $this->transformer->toWgs84('EPSG:25833', 500000.0, 6224460.0); + + $this->assertEqualsWithDelta(15.0, $longitude, 0.001); + } + + public function testItRejectsAnUnregisteredCrs(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/Unknown CRS "EPSG:31700"/'); + + $this->transformer->toWgs84('EPSG:31700', 500000.0, 6224460.0); + } + + public function testItAcceptsAdditionalDefinitions(): void + { + $transformer = new Wgs84Transformer([ + 'EPSG:23032' => '+proj=utm +zone=32 +ellps=intl +units=m +no_defs', + ]); + + [$longitude, $latitude] = $transformer->toWgs84('EPSG:23032', self::REFERENCE_EASTING, self::REFERENCE_NORTHING); + + // Different datum, so not identical to EPSG:25832 — but the same + // corner of the world, which proves the definition was registered. + $this->assertEqualsWithDelta(10.2152, $longitude, 0.01); + $this->assertEqualsWithDelta(56.1592, $latitude, 0.01); + } + + public function testItReprojectsAPointGeometry(): void + { + $geometry = $this->transformer->geometry(self::UTM32, [ + 'type' => 'Point', + 'coordinates' => [self::REFERENCE_EASTING, self::REFERENCE_NORTHING], + ]); + + $this->assertSame('Point', $geometry['type']); + $this->assertEqualsWithDelta(10.2152, $geometry['coordinates'][0], 0.001); + } + + public function testItReprojectsALineString(): void + { + $geometry = $this->transformer->geometry(self::UTM32, [ + 'type' => 'LineString', + 'coordinates' => [ + [574108.2557507273, 6222343.6199512165], + [self::REFERENCE_EASTING, self::REFERENCE_NORTHING], + ], + ]); + + $this->assertSame('LineString', $geometry['type']); + $this->assertCount(2, $geometry['coordinates']); + $this->assertEqualsWithDelta(10.1926, $geometry['coordinates'][0][0], 0.001); + $this->assertEqualsWithDelta(10.2152, $geometry['coordinates'][1][0], 0.001); + } + + public function testItReprojectsAPolygonPreservingNesting(): void + { + $geometry = $this->transformer->geometry(self::UTM32, [ + 'type' => 'Polygon', + 'coordinates' => [ + [ + [574108.0, 6222343.0], + [574208.0, 6222343.0], + [574208.0, 6222443.0], + [574108.0, 6222343.0], + ], + ], + ]); + + $this->assertSame('Polygon', $geometry['type']); + $this->assertCount(1, $geometry['coordinates']); + $this->assertCount(4, $geometry['coordinates'][0]); + $this->assertIsFloat($geometry['coordinates'][0][0][0]); + } + + public function testItReprojectsAMultiPolygon(): void + { + $ring = [ + [574108.0, 6222343.0], + [574208.0, 6222343.0], + [574208.0, 6222443.0], + [574108.0, 6222343.0], + ]; + + $geometry = $this->transformer->geometry(self::UTM32, [ + 'type' => 'MultiPolygon', + 'coordinates' => [[$ring], [$ring]], + ]); + + $this->assertCount(2, $geometry['coordinates']); + $this->assertCount(4, $geometry['coordinates'][1][0]); + } + + public function testItRejectsAGeometryWithoutCoordinates(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->transformer->geometry(self::UTM32, ['type' => 'Point']); + } + + public function testItRejectsAGeometryCollection(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->transformer->geometry(self::UTM32, [ + 'type' => 'GeometryCollection', + 'geometries' => [], + ]); + } + + public function testItRejectsAPositionWithASingleOrdinate(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->transformer->geometry(self::UTM32, ['type' => 'Point', 'coordinates' => [574108.0]]); + } +} diff --git a/tests/Source/FeedReaderTest.php b/tests/Source/FeedReaderTest.php new file mode 100644 index 0000000..d5487f3 --- /dev/null +++ b/tests/Source/FeedReaderTest.php @@ -0,0 +1,114 @@ +reader($client)->read('https://example.com/feed.json'); + + $this->assertSame([['example' => 1]], $document); + } + + public function testItAcceptsABareArrayDocument(): void + { + $client = new MockHttpClient(new MockResponse('[1, 2, 3]')); + + $this->assertSame([1, 2, 3], $this->reader($client)->read('http://example.com/feed.json')); + } + + /** + * The envelope is feed-specific knowledge, so it must survive intact for + * the caller to interpret. Returning the features list directly would push + * that knowledge into the wrong class. + */ + public function testItDoesNotUnwrapTheEnvelope(): void + { + $client = new MockHttpClient(new MockResponse(self::FEATURE_COLLECTION)); + + $document = $this->reader($client)->read('https://example.com/feed.json'); + + $this->assertArrayHasKey('features', $document); + $this->assertArrayNotHasKey(0, $document); + $this->assertCount(2, $document['features']); + } + + public function testItRejectsALocationThatIsNotAUrl(): void + { + $client = new MockHttpClient(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches('/must be an http\(s\) URL/'); + + try { + $this->reader($client)->read('data/feed.json'); + } finally { + // Rejection has to happen before the request, or a mistyped + // location becomes an opaque transport error instead. + $this->assertSame(0, $client->getRequestsCount()); + } + } + + /** + * A string that merely begins with "http" is not a URL. + */ + public function testItRejectsAPathThatMerelyStartsWithHttp(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches('/must be an http\(s\) URL/'); + + $this->reader()->read('https-export.json'); + } + + public function testItFailsOnInvalidJson(): void + { + $client = new MockHttpClient(new MockResponse('{ not json')); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches('/Invalid JSON/'); + + $this->reader($client)->read('https://example.com/feed.json'); + } + + public function testItFailsWhenTheDocumentIsAScalar(): void + { + $client = new MockHttpClient(new MockResponse('"just a string"')); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches('/Expected a JSON array or object/'); + + $this->reader($client)->read('https://example.com/feed.json'); + } + + public function testItWrapsTransportFailures(): void + { + $client = new MockHttpClient(new MockResponse('', ['http_code' => 500])); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches('/Could not fetch/'); + + $this->reader($client)->read('https://example.com/feed.json'); + } +} diff --git a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php new file mode 100644 index 0000000..88fc89b --- /dev/null +++ b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php @@ -0,0 +1,129 @@ +> */ + private array $entities; + + protected function setUp(): void + { + $source = new HandicapParking( + new Wgs84Transformer(), + // Never used: the fixture is read from disk, not over HTTP. + new MockHttpClient(), + 'data/handicapparkering.json', + self::PARKING_SITE_URN, + \dirname(__DIR__, 2), + ); + + $this->entities = array_map( + static fn (NgsiEntity $entity): array => $entity->toArray(['https://example.com/context.jsonld']), + iterator_to_array($source->entities(), false) + ); + } + + public function testItReadsEveryRecordInTheFixture(): void + { + $this->assertCount(10, $this->entities); + } + + public function testItBuildsAParkingGroupWithAStableId(): void + { + $first = $this->entities[0]; + + // The id is derived from mi_prinx so that re-importing upserts the + // same entity instead of creating a duplicate. + $this->assertSame('urn:ngsi-ld:ParkingGroup:aarhus-handicap-261', $first['id']); + $this->assertSame('ParkingGroup', $first['type']); + } + + public function testItJoinsStreetAndHouseNumberIntoName(): void + { + $this->assertSame('P.P. Ørums Gade 2', $this->entities[0]['name']['value']); + } + + public function testItOmitsTheHouseNumberWhenBlank(): void + { + $brammersgade = $this->entityById('urn:ngsi-ld:ParkingGroup:aarhus-handicap-378'); + + // husnnr is "" for this row, so the name must not end in a space. + $this->assertSame('Brammersgade', $brammersgade['name']['value']); + } + + public function testItMarksEveryGroupAsOnStreetDisabledParking(): void + { + foreach ($this->entities as $entity) { + $this->assertSame(['onStreet', 'onlyDisabled'], $entity['category']['value']); + } + } + + public function testItCarriesTheBayCountAsTotalSpotNumber(): void + { + $this->assertSame(2, $this->entities[0]['totalSpotNumber']['value']); + $this->assertSame(1, $this->entities[1]['totalSpotNumber']['value']); + } + + /** + * The feed stores dates as Excel serial numbers with a decimal comma + * ("43543,6190690972"), which has to become a real timestamp. + */ + public function testItConvertsExcelSerialDatesToIso8601(): void + { + $observedAt = $this->entities[0]['totalSpotNumber']['observedAt']; + + $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $observedAt); + $this->assertStringStartsWith('2019-03-', $observedAt); + } + + public function testItEmitsLocationAsAGeoProperty(): void + { + $location = $this->entities[0]['location']; + + $this->assertSame('GeoProperty', $location['type']); + $this->assertSame('Point', $location['value']['type']); + } + + public function testItDropsEmptyDescriptions(): void + { + // bemrk is "" for the first row and "Navitas" for mi_prinx 215. + $this->assertArrayNotHasKey('description', $this->entities[0]); + $this->assertSame( + 'Navitas', + $this->entityById('urn:ngsi-ld:ParkingGroup:aarhus-handicap-215')['description']['value'] + ); + } + + public function testItRelatesEveryGroupToTheSyntheticParkingSite(): void + { + foreach ($this->entities as $entity) { + $this->assertSame('Relationship', $entity['refParkingSite']['type']); + $this->assertSame(self::PARKING_SITE_URN, $entity['refParkingSite']['object']); + } + } + + /** + * @return array + */ + private function entityById(string $id): array + { + foreach ($this->entities as $entity) { + if ($id === $entity['id']) { + return $entity; + } + } + + $this->fail(\sprintf('No entity with id "%s".', $id)); + } +} From db7575b30682875ccc40032ac28995628efde362 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 14:06:09 +0200 Subject: [PATCH 020/101] Renaming paths to new source location structure --- .env | 4 ++-- README.md | 22 +++++++++++----------- Taskfile.yml | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.env b/.env index 2dfb180..5e53eba 100644 --- a/.env +++ b/.env @@ -44,6 +44,6 @@ ENTER_NGSI_CONTEXT_URLS='https://raw.githubusercontent.com/smart-data-models/dat ENTER_NGSI_DOMAIN_CONTEXT=https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld # Where the disabled-parking export is read from: the live SpatialMap export. -# Accepts any http(s) URL, or a path relative to the project directory. -ENTER_HANDICAP_PARKING_SOURCE='https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' +# Must be an http(s) URL — feeds are read where they live, never from a copy. +ENTER_MTM_SPATIALMAPS_HANDICAP_PARKING_SOURCE='https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' ###< app ### diff --git a/README.md b/README.md index 4052b40..b6a8d5f 100644 --- a/README.md +++ b/README.md @@ -26,20 +26,20 @@ source feed (JSON) → context broker ``` -| Class | Responsibility | -| ---------------------------------- | ------------------------------------------------ | -| `App\Source\SourceInterface` | Contract for one input data set | -| `App\Source\HandicapParkingSource` | Disabled parking bays → `OnStreetParking` | -| `App\Source\FeedReader` | Path or URL → decoded JSON | -| `App\Geo\Wgs84Transformer` | Any registered CRS → WGS84, any GeoJSON geometry | -| `App\Ngsi\NgsiEntity` | Builds normalized NGSI-LD entities | -| `App\Broker\NgsiLdBroker` | Batch upsert to the broker | -| `App\Command\ImportCommand` | `app:import` | +| Class | Responsibility | +| ------------------------------------------- | ------------------------------------------------ | +| `App\Source\SourceInterface` | Contract for one input data set | +| `App\Source\FeedReader` | Feed URL → decoded JSON | +| `App\Source\MtmSpatialMaps\HandicapParking` | Disabled parking bays → `OnStreetParking` | +| `App\Geo\Wgs84Transformer` | Any registered CRS → WGS84, any GeoJSON geometry | +| `App\Ngsi\NgsiEntity` | Builds normalized NGSI-LD entities | +| `App\Broker\NgsiLdBroker` | Batch upsert to the broker | +| `App\Command\ImportCommand` | `app:import` | ``` shell task import # list the available sources -task import -- MTM-handicap-parking # import one -task import -- MTM-handicap-parking --dry-run --limit 5 # print the payload instead +task import -- mtm_spatialmaps-handicap-parking # import one +task import -- mtm_spatialmaps-handicap-parking --dry-run --limit 5 # print the payload instead task broker:entities -- OnStreetParking 10 # read back what landed ``` diff --git a/Taskfile.yml b/Taskfile.yml index 3b30260..6ac1694 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -37,7 +37,7 @@ tasks: test:application: *test_task import: - desc: 'Import a source into the broker, e.g. task import -- MTM-handicap-parking' + desc: 'Import a source into the broker, e.g. task import -- mtm_spatialmaps-handicap-parking' cmd: ddev console app:import {{.CLI_ARGS}} # The broker itself needs no start task: Scorpio is a DDEV custom service From 8e4f7ca049ed80907c4046190acd1cef0bb9f4d3 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 14:07:15 +0200 Subject: [PATCH 021/101] Added pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e39b258 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ + Date: Tue, 1 Sep 2026 14:23:32 +0200 Subject: [PATCH 022/101] Coding standards --- src/Broker/NgsiLdBroker.php | 10 +++++----- src/Command/ImportCommand.php | 3 +-- src/Geo/Wgs84Transformer.php | 4 ++-- src/Source/FeedReader.php | 4 ++-- src/Source/MtmSpatialMaps/HandicapParking.php | 10 +++++----- tests/Command/ImportCommandTest.php | 8 ++++---- tests/Geo/Wgs84TransformerTest.php | 6 +++--- tests/Source/FeedReaderTest.php | 2 +- tests/Source/MtmSpatialMaps/HandicapParkingTest.php | 2 +- 9 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/Broker/NgsiLdBroker.php b/src/Broker/NgsiLdBroker.php index 2ce7651..806a99f 100644 --- a/src/Broker/NgsiLdBroker.php +++ b/src/Broker/NgsiLdBroker.php @@ -15,21 +15,21 @@ * import idempotent, which matters because the entity ids are derived from * the source's own primary key. */ -final class NgsiLdBroker +final readonly class NgsiLdBroker { - private const UPSERT_PATH = '/ngsi-ld/v1/entityOperations/upsert'; + private const string UPSERT_PATH = '/ngsi-ld/v1/entityOperations/upsert'; /** * The payload carries its own @context, so it must be sent as * application/ld+json. Sending application/json instead requires the * context in a Link header, and brokers reject the mismatch. */ - private const CONTENT_TYPE = 'application/ld+json'; + private const string CONTENT_TYPE = 'application/ld+json'; public function __construct( - private readonly HttpClientInterface $client, + private HttpClientInterface $client, #[Autowire(env: 'APP_BROKER_BASE_URI')] - private readonly string $brokerUrl, + private string $brokerUrl, ) { } diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php index 1dab064..b0c6a46 100644 --- a/src/Command/ImportCommand.php +++ b/src/Command/ImportCommand.php @@ -6,7 +6,6 @@ use App\Broker\NgsiLdBroker; use App\Source\SourceInterface; -use MongoDB\Driver\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -153,6 +152,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function contexts(): array { - return array_values(array_filter(array_map('trim', explode(',', $this->contextUrls)))); + return array_values(array_filter(array_map(trim(...), explode(',', $this->contextUrls)))); } } diff --git a/src/Geo/Wgs84Transformer.php b/src/Geo/Wgs84Transformer.php index 9f64596..4894555 100644 --- a/src/Geo/Wgs84Transformer.php +++ b/src/Geo/Wgs84Transformer.php @@ -15,7 +15,7 @@ */ final class Wgs84Transformer { - public const TARGET_SRID = 'EPSG:4326'; + public const string TARGET_SRID = 'EPSG:4326'; /** * PROJ definitions for coordinate systems this application reads. @@ -26,7 +26,7 @@ final class Wgs84Transformer * * @var array */ - private const DEFINITIONS = [ + private const array DEFINITIONS = [ // ETRS89 / UTM zone 32N — most of Denmark. 'EPSG:25832' => '+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', // ETRS89 / UTM zone 33N — Bornholm and eastwards. diff --git a/src/Source/FeedReader.php b/src/Source/FeedReader.php index 6f8677c..6a9c280 100644 --- a/src/Source/FeedReader.php +++ b/src/Source/FeedReader.php @@ -9,10 +9,10 @@ /** * Fetches a feed over HTTP and decodes it. */ -final class FeedReader +final readonly class FeedReader { public function __construct( - private readonly HttpClientInterface $client, + private HttpClientInterface $client, ) { } diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 62414b5..296340b 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -21,18 +21,18 @@ * * @see https://github.com/smart-data-models/dataModel.Parking/tree/master/OnStreetParking */ -final class HandicapParking implements SourceInterface +final readonly class HandicapParking implements SourceInterface { /** * The CRS(coordinate reference system) this feed publishes - SRID (Spatial reference identifier). */ - private const SOURCE_SRID = 'EPSG:25832'; + private const string SOURCE_SRID = 'EPSG:25832'; public function __construct( - private readonly FeedReader $reader, - private readonly Wgs84Transformer $transformer, + private FeedReader $reader, + private Wgs84Transformer $transformer, #[Autowire(env: 'ENTER_MTM_SPATIALMAPS_HANDICAP_PARKING_SOURCE')] - private readonly string $location, + private string $location, ) { } diff --git a/tests/Command/ImportCommandTest.php b/tests/Command/ImportCommandTest.php index 23830aa..ae858a3 100644 --- a/tests/Command/ImportCommandTest.php +++ b/tests/Command/ImportCommandTest.php @@ -29,11 +29,11 @@ private function tester(iterable $sources): CommandTester private function source(string $key, NgsiEntity ...$entities): SourceInterface { - return new class($key, $entities) implements SourceInterface { + return new readonly class($key, $entities) implements SourceInterface { /** @param list $entities */ public function __construct( - private readonly string $key, - private readonly array $entities, + private string $key, + private array $entities, ) { } @@ -120,7 +120,7 @@ public function testItRejectsAnUnknownSource(): void public function testDryRunPrintsThePayloadAndSendsNothing(): void { - $entity = (new NgsiEntity('urn:ngsi-ld:Example:1', 'Example')) + $entity = new NgsiEntity('urn:ngsi-ld:Example:1', 'Example') ->property('name', 'Example'); $tester = $this->tester([$this->source('one-entity', $entity)]); diff --git a/tests/Geo/Wgs84TransformerTest.php b/tests/Geo/Wgs84TransformerTest.php index 7e3d823..aacf039 100644 --- a/tests/Geo/Wgs84TransformerTest.php +++ b/tests/Geo/Wgs84TransformerTest.php @@ -9,15 +9,15 @@ class Wgs84TransformerTest extends TestCase { - private const UTM32 = 'EPSG:25832'; + private const string UTM32 = 'EPSG:25832'; /** * Reference coordinates for a building on the Aarhus harbourfront, known * independently to sit at roughly 56.1592 N, 10.2152 E. This is the * ground truth that catches a broken or missing CRS definition. */ - private const REFERENCE_EASTING = 575477.5441628407; - private const REFERENCE_NORTHING = 6224460.129141892; + private const float REFERENCE_EASTING = 575477.5441628407; + private const float REFERENCE_NORTHING = 6224460.129141892; private Wgs84Transformer $transformer; diff --git a/tests/Source/FeedReaderTest.php b/tests/Source/FeedReaderTest.php index d5487f3..8d925b8 100644 --- a/tests/Source/FeedReaderTest.php +++ b/tests/Source/FeedReaderTest.php @@ -16,7 +16,7 @@ class FeedReaderTest extends TestCase * properties, because nothing this class does depends on what the feed * contains — only on the envelope surviving. */ - private const FEATURE_COLLECTION = '{"type":"FeatureCollection","features":[{"example":1},{"example":2}]}'; + private const string FEATURE_COLLECTION = '{"type":"FeatureCollection","features":[{"example":1},{"example":2}]}'; private function reader(?MockHttpClient $client = null): FeedReader { diff --git a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php index 88fc89b..67200f2 100644 --- a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php +++ b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php @@ -12,7 +12,7 @@ class HandicapParkingTest extends TestCase { - private const PARKING_SITE_URN = 'urn:ngsi-ld:ParkingSite:aarhus-on-street'; + private const string PARKING_SITE_URN = 'urn:ngsi-ld:ParkingSite:aarhus-on-street'; /** @var list> */ private array $entities; From b04cb634ceb00c66b7308c952b962ee7e03b48e3 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 1 Sep 2026 14:39:47 +0200 Subject: [PATCH 023/101] Converted all ADRs in readme to draft --- docs/adr/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 1ce7c5e..57f6629 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -4,14 +4,14 @@ This directory contains Architecture Decision Records (ADRs) for this project. See [adr.github.io](https://adr.github.io/) for background on the format. -| Number | Title | Status | Date | -| ----------------------------------------------- | ------------------------------------------------------- | -------- | ---------- | -| [001](001-architecture-symfony-docker.md) | Architecture — Symfony 8 on the ITK Dev Docker template | Accepted | 2026-08-24 | -| [002](002-publish-to-a-context-broker.md) | Publication mechanism — publish to a context broker | Accepted | 2026-08-24 | -| [003](003-ngsi-ld-representation.md) | NGSI-LD representation — normalized form, batch upsert | Accepted | 2026-08-24 | -| [004](004-coordinate-reference-system.md) | Coordinate reference system — publish WGS84 | Accepted | 2026-08-27 | -| [005](005-smart-data-models-as-vocabulary.md) | Vocabulary — adopt Smart Data Models | Accepted | 2026-08-31 | -| [006](006-onstreetparking-over-parkinggroup.md) | Model selection — OnStreetParking over ParkingGroup | Accepted | 2026-08-31 | +| Number | Title | Status | Date | +| ----------------------------------------------- | ------------------------------------------------------- | ------ | ---------- | +| [001](001-architecture-symfony-docker.md) | Architecture — Symfony 8 on the ITK Dev Docker template | Draft | 2026-08-24 | +| [002](002-publish-to-a-context-broker.md) | Publication mechanism — publish to a context broker | Draft | 2026-08-24 | +| [003](003-ngsi-ld-representation.md) | NGSI-LD representation — normalized form, batch upsert | Draft | 2026-08-24 | +| [004](004-coordinate-reference-system.md) | Coordinate reference system — publish WGS84 | Draft | 2026-08-27 | +| [005](005-smart-data-models-as-vocabulary.md) | Vocabulary — adopt Smart Data Models | Draft | 2026-08-31 | +| [006](006-onstreetparking-over-parkinggroup.md) | Model selection — OnStreetParking over ParkingGroup | Draft | 2026-08-31 | Numbering follows dependency order: each ADR relies only on lower-numbered ones. Dates therefore do not run in the same order as numbers. From 22d50170465069d59f90477df075adad3e8342ea Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 4 Sep 2026 12:24:39 +0200 Subject: [PATCH 024/101] Added ADR 007: data set metadata in a committed source manifest --- docs/adr/007-source-manifest.md | 122 ++++++++++++++++++++++++++++++++ docs/adr/README.md | 4 +- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 docs/adr/007-source-manifest.md diff --git a/docs/adr/007-source-manifest.md b/docs/adr/007-source-manifest.md new file mode 100644 index 0000000..241eb8f --- /dev/null +++ b/docs/adr/007-source-manifest.md @@ -0,0 +1,122 @@ +# 007: Data set metadata — a committed source manifest + +| Field | Value | +|--------------------|--------------------------------------------------------| +| **Created By** | Jeppe Krogh | +| **Date** | 2026-09-02 | +| **Decision Maker** | ITK Dev team | +| **Stakeholders** | ITK Dev developers, data consumers, future maintainers | +| **Status** | Draft | + +## Context + +Each published data set carries facts the import needs — where the feed is read +from, the coordinate reference system its coordinates are in, and the model it +is published as — and facts only people need: who owns the data, on what terms +it may be republished, how often it changes, and which of its fields are +deliberately not published, with the reason for each. + +The first group must be readable by code. The second is what a public data +portal requires at registration, and what a data owner asks for when +establishing what happened to their data. Both grow with the number of data +sets. + +Recording the two groups separately produces a machine-readable value and a +written description of the same value, which can then disagree. Recording only +the first leaves the rest unwritten, and the questions it answers are then +answered from memory. + +This ADR serves to decide where a data set's own facts are recorded, and which +of them the code reads. + +### Drivers + +- **Functional:** the code reads the facts it needs from the same record a + person reads; a data set is registerable on a public portal without a fresh + survey; an incomplete record fails the import that needs it rather than + producing an incomplete publication. +- **Non-functional:** adding a data set requires no deployment change; the + record is reviewable as a diff; no fact exists in two places. + +### Options Considered + +1. **One environment variable per data set.** Follows the convention that + configuration belongs in the environment, and lets a value differ per + environment. But variable names grow with the catalogue, so each new data + set becomes a deployment change; the environment carries strings only, + leaving metadata beyond an address nowhere to live; and values are invisible + in review, so a wrong one is found by running the import. +2. **Every fact in the class that maps the data set.** Nothing can diverge, + there being one copy, and the language enforces its presence. But metadata + is then readable only by opening code, extracting it for portal registration + requires writing an extractor, and correcting a licence or a contact becomes + a code change reviewed as one. +3. **A committed manifest the code reads.** One record per data set, keyed by + the identifier the import selects it with, holding the facts the code needs + beside those it does not, in a shape a catalogue profile can be generated + from. Validating the record becomes work of our own, and the values are + identical in every environment. +4. **An external catalogue or registry service.** The eventual home of + published metadata, with search and harvesting already built. But it has to + be running for an import to work, it is a second system to operate, and it + must be populated before anything can be published from it — from records + that would have to live somewhere else in the meantime. + +## Decision + +Record each data set in a **committed manifest**, keyed by the identifier the +import selects it with, and read from it every fact the code needs. + +Four rules follow: + +1. **Record only what the code cannot state.** How a feed's fields map onto the + model, and every quirk of its shape, stay in the class that performs the + mapping. Restating them in the manifest recreates the divergence the + manifest exists to prevent. +2. **A fact both the code and a reader need is read from the manifest.** It is + not also written in code, in a comment, or in the README. +3. **An incomplete or malformed record is an error.** Fields an import cannot + run without are required, and their absence raises rather than defaulting. A + fact that is unknown is recorded as unknown, so the gap stays visible. +4. **Name the fields after the catalogue profile the data will be registered + under** — DCAT-AP — so that publication is a translation rather than a + redesign. + +Rationale: + +- What has to be prevented is a value diverging from its description, so the + two belong to the same record. +- Portal registration and answering a data owner need the same fields, and + neither can be derived from mapping code. +- A record is reviewed alongside the class that consumes it, so a reviewer sees + the address, the reference system and the model together with the mapping + that assumes them. +- Where a feed is read from is a fact about the data set, not about the machine + running the import, so the environment is the wrong place for it. + +## Consequences + +### Positive + +- One record per data set, reviewable as a diff and versioned with the code. +- Adding a data set is one class and one record, with no deployment change. +- Registration on a public portal is a translation of records that exist. +- Licence, ownership and what was withheld have a single answer, and an + unanswered question shows as an empty value rather than as nothing at all. +- The same records can generate catalogue entities later without introducing a + second source of truth. + +### Negative / Trade-offs + +- **Values are identical in every environment.** Pointing a data set at a copy + for testing means editing a committed file. That is consistent with reading + feeds where they live, but it removes an escape hatch the environment offered. +- **A wrong reference system or model in the manifest is as damaging as a wrong + one in code, while looking less like code.** A wrong reference system yields + coordinates that are well-formed and in the wrong place. +- **Changing the published model changes entity identifiers.** Editing one + value can therefore orphan everything already published; see ADR 005 and ADR + 006. +- **Fields no code reads have nothing keeping them current.** Until catalogue + entities are generated from them, only review does. +- Validating the record is work the environment did not require. diff --git a/docs/adr/README.md b/docs/adr/README.md index 57f6629..0560124 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -12,9 +12,11 @@ See [adr.github.io](https://adr.github.io/) for background on the format. | [004](004-coordinate-reference-system.md) | Coordinate reference system — publish WGS84 | Draft | 2026-08-27 | | [005](005-smart-data-models-as-vocabulary.md) | Vocabulary — adopt Smart Data Models | Draft | 2026-08-31 | | [006](006-onstreetparking-over-parkinggroup.md) | Model selection — OnStreetParking over ParkingGroup | Draft | 2026-08-31 | +| [007](007-source-manifest.md) | Data set metadata — a committed source manifest | Draft | 2026-09-02 | Numbering follows dependency order: each ADR relies only on lower-numbered ones. Dates therefore do not run in the same order as numbers. All ADRs state general policy and name no data set. Concrete per-data-set -mappings are recorded in the README and in the source classes themselves. +facts are recorded in `config/sources.yaml`, and the mappings in the source +classes themselves. From 8eb2a3709afea782ac5c0a84896bc4b0a06ed4ee Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 4 Sep 2026 12:24:46 +0200 Subject: [PATCH 025/101] Added source manifest and catalog reader One record per data set in config/sources.yaml, DCAT-AP field names. SourceCatalog validates lazily and fails loudly on incomplete records. --- config/sources.yaml | 46 ++++++++ src/Source/SourceCatalog.php | 169 +++++++++++++++++++++++++++++ src/Source/SourceDescriptor.php | 43 ++++++++ tests/Source/SourceCatalogTest.php | 144 ++++++++++++++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 config/sources.yaml create mode 100644 src/Source/SourceCatalog.php create mode 100644 src/Source/SourceDescriptor.php create mode 100644 tests/Source/SourceCatalogTest.php diff --git a/config/sources.yaml b/config/sources.yaml new file mode 100644 index 0000000..8081a03 --- /dev/null +++ b/config/sources.yaml @@ -0,0 +1,46 @@ +# The data sets this application publishes, one entry per source key. +# +# The facts the import needs — where the feed is read from, the coordinate +# reference system it publishes, the Smart Data Model it is published as — are +# read from here, so they exist in exactly one place. +# +# The remaining fields record what no code can state: who owns the data, on +# what terms, how often it changes, and which source fields are deliberately +# not published. The rule is to record only what the code cannot tell you, so +# the field mapping itself stays in the source class, which is the only place +# that knows the feed's shape. +# +# Field names follow DCAT-AP, the metadata profile European data portals +# harvest, so registering a data set is a translation of its entry rather than +# a new survey. See docs/adr/007-source-manifest.md. + +sources: + mtm_spatialmaps-handicap-parking: + title: 'Handicapparkering, Aarhus Kommune' + description: >- + Disabled parking bays in Aarhus Municipality, with the number of + reserved bays per location. + publisher: 'Aarhus Kommune' + contact: ppg@aarhus.dk + landing_page: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune' + access_url: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' + media_type: application/geo+json + crs: 'EPSG:25832' + model: OnStreetParking + update_frequency: continuous + + # The portal states no licence for this data set. DCAT-AP requires + # one, so it has to be settled with the data owner before the + # catalogue can be registered anywhere. + licence: ~ + + # Fields the feed carries that are not published. Recorded here + # because the source class shows what is mapped but cannot show what + # was left out, or why. + omitted_fields: + ident: 'Single-letter code; its meaning is not documented and not confirmed by the data owner.' + oprettet_af: 'Directory username of the municipal employee who created the record — personal data.' + rettet_af: 'Directory username of the municipal employee who last edited the record — personal data.' + oprettet_dato: 'Describes the register record rather than the parking bay.' + rettet_dato: 'Describes the register record rather than the parking bay. A candidate for observedAt, not yet mapped.' + mi_style: 'MapInfo rendering style, empty throughout the export.' diff --git a/src/Source/SourceCatalog.php b/src/Source/SourceCatalog.php new file mode 100644 index 0000000..cdcd9f9 --- /dev/null +++ b/src/Source/SourceCatalog.php @@ -0,0 +1,169 @@ +|null */ + private ?array $descriptors = null; + + public function __construct( + #[Autowire('%kernel.project_dir%/config/sources.yaml')] + private readonly string $manifest, + ) { + } + + /** + * @throws \RuntimeException when the manifest cannot be read, or carries no entry for the key + */ + public function get(string $key): SourceDescriptor + { + $descriptors = $this->all(); + + if (!isset($descriptors[$key])) { + throw new \RuntimeException(\sprintf('No entry for source "%s" in %s. Entries: %s.', $key, $this->manifest, [] === $descriptors ? 'none' : implode(', ', array_keys($descriptors)))); + } + + return $descriptors[$key]; + } + + /** + * @return array keyed by source key + * + * @throws \RuntimeException when the manifest cannot be read + */ + public function all(): array + { + return $this->descriptors ??= $this->load(); + } + + /** + * @return array + */ + private function load(): array + { + if (!is_file($this->manifest)) { + throw new \RuntimeException(\sprintf('Source manifest "%s" does not exist.', $this->manifest)); + } + + try { + $parsed = Yaml::parseFile($this->manifest); + } catch (ParseException $exception) { + throw new \RuntimeException(\sprintf('Source manifest "%s" is not valid YAML: %s', $this->manifest, $exception->getMessage()), previous: $exception); + } + + // Without this the wrong shape yields an empty catalogue, which reads + // as "no data sets are registered" rather than as a broken file. + $sources = \is_array($parsed) ? $parsed['sources'] ?? null : null; + if (!\is_array($sources)) { + throw new \RuntimeException(\sprintf('Source manifest "%s" must contain a "sources" mapping at the top level.', $this->manifest)); + } + + $descriptors = []; + foreach ($sources as $key => $entry) { + $key = (string) $key; + + if (!\is_array($entry)) { + throw new \RuntimeException(\sprintf('Entry "%s" in %s must be a mapping, got %s.', $key, $this->manifest, get_debug_type($entry))); + } + + $descriptors[$key] = $this->descriptor($key, $entry); + } + + return $descriptors; + } + + /** + * @param array $entry + */ + private function descriptor(string $key, array $entry): SourceDescriptor + { + return new SourceDescriptor( + key: $key, + title: $this->required($key, $entry, 'title'), + accessUrl: $this->required($key, $entry, 'access_url'), + crs: $this->required($key, $entry, 'crs'), + model: $this->required($key, $entry, 'model'), + description: $this->optional($key, $entry, 'description'), + publisher: $this->optional($key, $entry, 'publisher'), + contact: $this->optional($key, $entry, 'contact'), + landingPage: $this->optional($key, $entry, 'landing_page'), + mediaType: $this->optional($key, $entry, 'media_type'), + updateFrequency: $this->optional($key, $entry, 'update_frequency'), + licence: $this->optional($key, $entry, 'licence'), + omittedFields: $this->omittedFields($key, $entry), + ); + } + + /** + * @param array $entry + */ + private function required(string $key, array $entry, string $field): string + { + return $this->optional($key, $entry, $field) + ?? throw new \RuntimeException(\sprintf('Entry "%s" in %s is missing the required field "%s"; an import cannot run without it.', $key, $this->manifest, $field)); + } + + /** + * An empty value means "not filled in", the same as an absent key, so both + * become null rather than an empty string. + * + * @param array $entry + */ + private function optional(string $key, array $entry, string $field): ?string + { + $value = $entry[$field] ?? null; + + if (null === $value || '' === $value) { + return null; + } + + if (!is_scalar($value)) { + throw new \RuntimeException(\sprintf('Field "%s" of entry "%s" in %s must be a single value, got %s.', $field, $key, $this->manifest, get_debug_type($value))); + } + + return trim((string) $value); + } + + /** + * @param array $entry + * + * @return array + */ + private function omittedFields(string $key, array $entry): array + { + $omitted = $entry['omitted_fields'] ?? []; + + if (!\is_array($omitted)) { + throw new \RuntimeException(\sprintf('Field "omitted_fields" of entry "%s" in %s must map each field to the reason it is not published.', $key, $this->manifest)); + } + + $reasons = []; + foreach ($omitted as $field => $reason) { + // The reason is the half of the record that cannot be recovered + // from the code, so a bare list of names is not accepted. + if (!\is_string($reason) || '' === trim($reason)) { + throw new \RuntimeException(\sprintf('Omitted field "%s" of entry "%s" in %s needs a reason.', $field, $key, $this->manifest)); + } + + $reasons[(string) $field] = trim($reason); + } + + return $reasons; + } +} diff --git a/src/Source/SourceDescriptor.php b/src/Source/SourceDescriptor.php new file mode 100644 index 0000000..83b2b14 --- /dev/null +++ b/src/Source/SourceDescriptor.php @@ -0,0 +1,43 @@ + $omittedFields source field name => why it is not published + */ + public function __construct( + public string $key, + public string $title, + public string $accessUrl, + public string $crs, + public string $model, + public ?string $description = null, + public ?string $publisher = null, + public ?string $contact = null, + public ?string $landingPage = null, + public ?string $mediaType = null, + public ?string $updateFrequency = null, + public ?string $licence = null, + public array $omittedFields = [], + ) { + } +} diff --git a/tests/Source/SourceCatalogTest.php b/tests/Source/SourceCatalogTest.php new file mode 100644 index 0000000..e5f1dbd --- /dev/null +++ b/tests/Source/SourceCatalogTest.php @@ -0,0 +1,144 @@ + */ + private array $written = []; + + protected function tearDown(): void + { + foreach ($this->written as $path) { + if (is_file($path)) { + unlink($path); + } + } + + $this->written = []; + } + + public function testTheShippedManifestIsUsable(): void + { + $catalog = new SourceCatalog(\dirname(__DIR__, 2).'/config/sources.yaml'); + + $this->assertNotSame([], $catalog->all(), 'The manifest registers no data sets.'); + } + + /** + * A wrong URL scheme or a CRS the transformer does not know only surfaces + * mid-import otherwise, after the feed has been fetched. + */ + public function testEveryShippedEntryCanBeImportedFrom(): void + { + $catalog = new SourceCatalog(\dirname(__DIR__, 2).'/config/sources.yaml'); + + foreach ($catalog->all() as $key => $descriptor) { + $this->assertSame($key, $descriptor->key); + $this->assertMatchesRegularExpression('#^https?://#', $descriptor->accessUrl, $key); + $this->assertMatchesRegularExpression('/^EPSG:\d+$/', $descriptor->crs, $key); + $this->assertNotSame('', $descriptor->model, $key); + } + } + + public function testItNamesTheKnownEntriesWhenAskedForAnUnknownOne(): void + { + $catalog = new SourceCatalog($this->manifest(<<<'YAML' + sources: + a-source: + title: A source + access_url: https://example.com/feed.json + crs: EPSG:25832 + model: Example + YAML)); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Entries: a-source.'); + + $catalog->get('no-such-source'); + } + + public function testItRejectsAManifestWithoutASourcesMapping(): void + { + $catalog = new SourceCatalog($this->manifest("data_sets:\n a-source: {}\n")); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('must contain a "sources" mapping'); + + $catalog->all(); + } + + public function testItRejectsAnEntryMissingAFieldTheImportNeeds(): void + { + $catalog = new SourceCatalog($this->manifest(<<<'YAML' + sources: + a-source: + title: A source + access_url: https://example.com/feed.json + model: Example + YAML)); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('missing the required field "crs"'); + + $catalog->all(); + } + + public function testItRejectsAnOmittedFieldWithoutAReason(): void + { + $catalog = new SourceCatalog($this->manifest(<<<'YAML' + sources: + a-source: + title: A source + access_url: https://example.com/feed.json + crs: EPSG:25832 + model: Example + omitted_fields: + some_field: ~ + YAML)); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('needs a reason'); + + $catalog->all(); + } + + public function testItReportsAManifestThatIsNotThere(): void + { + $catalog = new SourceCatalog('/no/such/sources.yaml'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('does not exist'); + + $catalog->all(); + } + + public function testItReportsUnparsableYaml(): void + { + $catalog = new SourceCatalog($this->manifest("sources:\n - [unbalanced\n")); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('is not valid YAML'); + + $catalog->all(); + } + + private function manifest(string $yaml): string + { + $path = tempnam(sys_get_temp_dir(), 'sources-'); + + if (false === $path) { + $this->fail('Could not create a temporary manifest.'); + } + + file_put_contents($path, $yaml); + $this->written[] = $path; + + return $path; + } +} From 8e3683218d6d6cd5feb048021059a957bae421a5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 4 Sep 2026 12:24:55 +0200 Subject: [PATCH 026/101] Moved handicap parking feed config from .env to the source manifest Removes ENTER_MTM_SPATIALMAPS_HANDICAP_PARKING_SOURCE; the feed URL, CRS and model now come from the manifest entry, and the entity type and URN prefix both derive from its model field. Rewrote the test, which called a constructor signature that no longer existed and read a fixture file not in the repo. --- .env | 6 +- src/Source/MtmSpatialMaps/HandicapParking.php | 43 ++--- .../MtmSpatialMaps/HandicapParkingTest.php | 169 ++++++++++++------ 3 files changed, 141 insertions(+), 77 deletions(-) diff --git a/.env b/.env index 5e53eba..a9d7b27 100644 --- a/.env +++ b/.env @@ -43,7 +43,7 @@ ENTER_NGSI_CONTEXT_URLS='https://raw.githubusercontent.com/smart-data-models/dat # being read — Parking while that is the only model published. ENTER_NGSI_DOMAIN_CONTEXT=https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld -# Where the disabled-parking export is read from: the live SpatialMap export. -# Must be an http(s) URL — feeds are read where they live, never from a copy. -ENTER_MTM_SPATIALMAPS_HANDICAP_PARKING_SOURCE='https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' +# Where each feed is read from is not configured here. It belongs to the data +# set rather than to the environment, and lives in config/sources.yaml — see +# docs/adr/007-source-manifest.md. ###< app ### diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 296340b..72e0653 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -7,47 +7,50 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\FeedReader; +use App\Source\SourceCatalog; +use App\Source\SourceDescriptor; use App\Source\SourceInterface; -use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * Disabled parking bays in Aarhus Municipality, exported from SpatialMap. - * https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap. * - * Published at site level as OnStreetParking rather than as a ParkingGroup - * subdivision: the feed describes locations with a count of reserved bays and - * nothing above them, and ParkingGroup requires a parent site this source does - * not contain. See ADR 006, and ADR 005 rule 2 for the principle behind it. + * Where the feed is read from, the CRS it publishes and the model it is + * published as come from this key's manifest entry; this class owns only the + * field mapping. * + * Published at site level rather than as a ParkingGroup subdivision: the feed + * describes locations with a count of reserved bays and nothing above them, + * and ParkingGroup requires a parent site this source does not contain. See + * ADR 006, and ADR 005 rule 2 for the principle behind it. + * + * @see config/sources.yaml * @see https://github.com/smart-data-models/dataModel.Parking/tree/master/OnStreetParking */ final readonly class HandicapParking implements SourceInterface { - /** - * The CRS(coordinate reference system) this feed publishes - SRID (Spatial reference identifier). - */ - private const string SOURCE_SRID = 'EPSG:25832'; + private const string KEY = 'mtm_spatialmaps-handicap-parking'; public function __construct( private FeedReader $reader, private Wgs84Transformer $transformer, - #[Autowire(env: 'ENTER_MTM_SPATIALMAPS_HANDICAP_PARKING_SOURCE')] - private string $location, + private SourceCatalog $catalog, ) { } public function key(): string { - return 'mtm_spatialmaps-handicap-parking'; + return self::KEY; } public function entities(): iterable { + $source = $this->catalog->get(self::KEY); + // The export is a GeoJSON FeatureCollection, so the records live under // `features`. Iterating the document itself would walk its two // top-level keys instead. - foreach ($this->reader->read($this->location)['features'] ?? [] as $feature) { - if (\is_array($feature) && null !== $entity = $this->toEntity($feature)) { + foreach ($this->reader->read($source->accessUrl)['features'] ?? [] as $feature) { + if (\is_array($feature) && null !== $entity = $this->toEntity($feature, $source)) { yield $entity; } } @@ -56,7 +59,7 @@ public function entities(): iterable /** * @param array $feature GeoJSON Feature */ - private function toEntity(array $feature): ?NgsiEntity + private function toEntity(array $feature, SourceDescriptor $source): ?NgsiEntity { // A Feature keeps its attributes under `properties` and its geometry // beside them, so neither is at the feature's top level. @@ -76,8 +79,8 @@ private function toEntity(array $feature): ?NgsiEntity } $entity = new NgsiEntity( - \sprintf('urn:ngsi-ld:OnStreetParking:aarhus-handicap-%s', $key), - 'OnStreetParking' + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-%s', $source->model, $key), + $source->model ); // `forDisabled` rather than ParkingGroup's `onlyDisabled`: the two @@ -88,8 +91,8 @@ private function toEntity(array $feature): ?NgsiEntity ->property('description', trim((string) ($row['bemrk'] ?? ''))) ->property('category', ['forDisabled']) ->property('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) - ->property('source', $this->location) - ->geoProperty('location', $this->transformer->geometry(self::SOURCE_SRID, $geometry)); + ->property('source', $source->accessUrl) + ->geoProperty('location', $this->transformer->geometry($source->crs, $geometry)); } /** diff --git a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php index 67200f2..e0065a7 100644 --- a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php +++ b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php @@ -6,27 +6,41 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; +use App\Source\FeedReader; use App\Source\MtmSpatialMaps\HandicapParking; +use App\Source\SourceCatalog; +use App\Source\SourceDescriptor; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +/** + * Runs against the manifest the project ships, not a fixture of one, so the + * entry this source depends on is covered too. Only the feed is mocked. + */ class HandicapParkingTest extends TestCase { - private const string PARKING_SITE_URN = 'urn:ngsi-ld:ParkingSite:aarhus-on-street'; + private const string KEY = 'mtm_spatialmaps-handicap-parking'; + + private SourceDescriptor $source; + + private string $requestedUrl; /** @var list> */ private array $entities; protected function setUp(): void { - $source = new HandicapParking( - new Wgs84Transformer(), - // Never used: the fixture is read from disk, not over HTTP. - new MockHttpClient(), - 'data/handicapparkering.json', - self::PARKING_SITE_URN, - \dirname(__DIR__, 2), - ); + $catalog = new SourceCatalog(\dirname(__DIR__, 3).'/config/sources.yaml'); + $this->source = $catalog->get(self::KEY); + + $client = new MockHttpClient(function (string $method, string $url): MockResponse { + $this->requestedUrl = $url; + + return new MockResponse(json_encode($this->feed(), \JSON_THROW_ON_ERROR)); + }); + + $source = new HandicapParking(new FeedReader($client), new Wgs84Transformer(), $catalog); $this->entities = array_map( static fn (NgsiEntity $entity): array => $entity->toArray(['https://example.com/context.jsonld']), @@ -34,96 +48,143 @@ protected function setUp(): void ); } - public function testItReadsEveryRecordInTheFixture(): void + public function testItReadsTheFeedTheManifestPointsAt(): void + { + $this->assertSame($this->source->accessUrl, $this->requestedUrl); + } + + public function testItSkipsRecordsWithoutAnIdentifierOrGeometry(): void { - $this->assertCount(10, $this->entities); + // Four features, of which one has no mi_prinx and one no geometry. + $this->assertCount(2, $this->entities); } - public function testItBuildsAParkingGroupWithAStableId(): void + public function testItTakesIdentifierAndTypeFromTheManifestModel(): void { $first = $this->entities[0]; // The id is derived from mi_prinx so that re-importing upserts the - // same entity instead of creating a duplicate. - $this->assertSame('urn:ngsi-ld:ParkingGroup:aarhus-handicap-261', $first['id']); - $this->assertSame('ParkingGroup', $first['type']); + // same bay instead of creating a duplicate. + $this->assertSame( + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-172', $this->source->model), + $first['id'] + ); + $this->assertSame($this->source->model, $first['type']); } public function testItJoinsStreetAndHouseNumberIntoName(): void { - $this->assertSame('P.P. Ørums Gade 2', $this->entities[0]['name']['value']); + $this->assertSame('Domkirkeplads/Bispegade 1', $this->entities[0]['name']['value']); } public function testItOmitsTheHouseNumberWhenBlank(): void { - $brammersgade = $this->entityById('urn:ngsi-ld:ParkingGroup:aarhus-handicap-378'); - // husnnr is "" for this row, so the name must not end in a space. - $this->assertSame('Brammersgade', $brammersgade['name']['value']); + $this->assertSame('Brammersgade', $this->entities[1]['name']['value']); } - public function testItMarksEveryGroupAsOnStreetDisabledParking(): void + public function testItMarksEveryEntityAsDisabledParking(): void { foreach ($this->entities as $entity) { - $this->assertSame(['onStreet', 'onlyDisabled'], $entity['category']['value']); + $this->assertSame(['forDisabled'], $entity['category']['value']); } } public function testItCarriesTheBayCountAsTotalSpotNumber(): void { - $this->assertSame(2, $this->entities[0]['totalSpotNumber']['value']); + $this->assertSame(6, $this->entities[0]['totalSpotNumber']['value']); $this->assertSame(1, $this->entities[1]['totalSpotNumber']['value']); } - /** - * The feed stores dates as Excel serial numbers with a decimal comma - * ("43543,6190690972"), which has to become a real timestamp. - */ - public function testItConvertsExcelSerialDatesToIso8601(): void + public function testItDropsEmptyDescriptions(): void { - $observedAt = $this->entities[0]['totalSpotNumber']['observedAt']; - - $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $observedAt); - $this->assertStringStartsWith('2019-03-', $observedAt); + // bemrk is null for the first row and filled in for the second. + $this->assertArrayNotHasKey('description', $this->entities[0]); + $this->assertSame('Ved indgangen', $this->entities[1]['description']['value']); } - public function testItEmitsLocationAsAGeoProperty(): void + public function testItReprojectsLocationIntoWgs84(): void { $location = $this->entities[0]['location']; $this->assertSame('GeoProperty', $location['type']); $this->assertSame('Point', $location['value']['type']); - } - public function testItDropsEmptyDescriptions(): void - { - // bemrk is "" for the first row and "Navitas" for mi_prinx 215. - $this->assertArrayNotHasKey('description', $this->entities[0]); - $this->assertSame( - 'Navitas', - $this->entityById('urn:ngsi-ld:ParkingGroup:aarhus-handicap-215')['description']['value'] - ); + // The feed publishes metres in the manifest's CRS, so degrees within + // Denmark are the evidence that the reprojection ran. + [$longitude, $latitude] = $location['value']['coordinates']; + $this->assertGreaterThan(8.0, $longitude); + $this->assertLessThan(13.0, $longitude); + $this->assertGreaterThan(54.5, $latitude); + $this->assertLessThan(57.8, $latitude); } - public function testItRelatesEveryGroupToTheSyntheticParkingSite(): void + public function testItRecordsTheManifestUrlAsTheEntitySource(): void { - foreach ($this->entities as $entity) { - $this->assertSame('Relationship', $entity['refParkingSite']['type']); - $this->assertSame(self::PARKING_SITE_URN, $entity['refParkingSite']['object']); - } + $this->assertSame($this->source->accessUrl, $this->entities[0]['source']['value']); } /** + * The first feature is a record from the live export, kept verbatim. The + * rest are constructed to exercise a blank house number and the two guards + * that discard a record. + * * @return array */ - private function entityById(string $id): array + private function feed(): array { - foreach ($this->entities as $entity) { - if ($id === $entity['id']) { - return $entity; - } - } - - $this->fail(\sprintf('No entity with id "%s".', $id)); + return [ + 'type' => 'FeatureCollection', + 'crs' => ['type' => 'name', 'properties' => ['name' => 'EPSG:25832']], + 'features' => [ + [ + 'type' => 'Feature', + 'geometry' => ['type' => 'Point', 'coordinates' => [575153.9524951308, 6224260.609753487]], + 'properties' => [ + 'vejnavn' => 'Domkirkeplads/Bispegade', + 'husnnr' => '1', + 'invalidepladser' => 6, + 'bemrk' => null, + 'ident' => 'P', + 'oprettet_af' => 'ADM\\aztnbnd', + 'oprettet_dato' => '2019-03-19 14:51:23.91', + 'rettet_af' => 'ADM\\aztnbnd', + 'rettet_dato' => '2019-03-19 14:51:23.91', + 'mi_style' => null, + 'mi_prinx' => 172, + ], + ], + [ + 'type' => 'Feature', + 'geometry' => ['type' => 'Point', 'coordinates' => [574000.0, 6223000.0]], + 'properties' => [ + 'vejnavn' => 'Brammersgade', + 'husnnr' => '', + 'invalidepladser' => 1, + 'bemrk' => 'Ved indgangen', + 'mi_prinx' => 378, + ], + ], + [ + 'type' => 'Feature', + 'geometry' => ['type' => 'Point', 'coordinates' => [574100.0, 6223100.0]], + 'properties' => [ + 'vejnavn' => 'Uden nøgle', + 'husnnr' => '3', + 'invalidepladser' => 2, + ], + ], + [ + 'type' => 'Feature', + 'geometry' => null, + 'properties' => [ + 'vejnavn' => 'Uden geometri', + 'husnnr' => '5', + 'invalidepladser' => 2, + 'mi_prinx' => 999, + ], + ], + ], + ]; } } From 0bd7178b40eac6b54c88f2bcb9198f1a1cb732fa Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 4 Sep 2026 12:25:03 +0200 Subject: [PATCH 027/101] Updated README and changelog for the source manifest --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e140da..5758a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,5 +21,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `task broker:entities` for reading entities back out of the broker. - Architecture Decision Records under `docs/adr`. - Added test suite +- `config/sources.yaml`: one record per data set, holding the feed URL, its CRS + and the model it is published as alongside the metadata no code can state — + owner, contact, licence, update frequency, and the source fields deliberately + left unpublished with the reason for each. Field names follow DCAT-AP. +- `SourceCatalog` and `SourceDescriptor`: read and validate the manifest, + failing loudly on a missing or malformed record rather than defaulting. + +### Changed + +- Sources read their feed URL, CRS and model from `config/sources.yaml` instead + of holding them in an environment variable and a class constant. + +### Removed + +- `ENTER_MTM_SPATIALMAPS_HANDICAP_PARKING_SOURCE`, and with it the pattern of + one environment variable per data set. Where a feed is read from is a fact + about the data set, not about the environment. [Unreleased]: https://github.com/itk-dev/enter diff --git a/README.md b/README.md index b6a8d5f..511ead3 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,17 @@ source feed (JSON) → context broker ``` -| Class | Responsibility | -| ------------------------------------------- | ------------------------------------------------ | -| `App\Source\SourceInterface` | Contract for one input data set | -| `App\Source\FeedReader` | Feed URL → decoded JSON | -| `App\Source\MtmSpatialMaps\HandicapParking` | Disabled parking bays → `OnStreetParking` | -| `App\Geo\Wgs84Transformer` | Any registered CRS → WGS84, any GeoJSON geometry | -| `App\Ngsi\NgsiEntity` | Builds normalized NGSI-LD entities | -| `App\Broker\NgsiLdBroker` | Batch upsert to the broker | -| `App\Command\ImportCommand` | `app:import` | +| Class | Responsibility | +| ------------------------------------------- | ------------------------------------------------- | +| `App\Source\SourceInterface` | Contract for one input data set | +| `App\Source\SourceCatalog` | Reads the source manifest | +| `App\Source\SourceDescriptor` | One manifest entry: what a data set is and where | +| `App\Source\FeedReader` | Feed URL → decoded JSON | +| `App\Source\MtmSpatialMaps\HandicapParking` | Disabled parking bays → `OnStreetParking` | +| `App\Geo\Wgs84Transformer` | Any registered CRS → WGS84, any GeoJSON geometry | +| `App\Ngsi\NgsiEntity` | Builds normalized NGSI-LD entities | +| `App\Broker\NgsiLdBroker` | Batch upsert to the broker | +| `App\Command\ImportCommand` | `app:import` | ``` shell task import # list the available sources @@ -43,13 +45,30 @@ task import -- mtm_spatialmaps-handicap-parking --dry-run --limit 5 # print t task broker:entities -- OnStreetParking 10 # read back what landed ``` -Adding a data set means adding one `SourceInterface` implementation. It is -discovered through `#[AutoconfigureTag('app.source')]` and shows up as an -`app:import` argument with no further wiring. +### Source manifest + +Every data set is recorded in [config/sources.yaml](config/sources.yaml), keyed +by the identifier `app:import` takes as its argument. The feed URL, the +coordinate reference system it publishes and the Smart Data Model it is +published as are read from there, so each exists in one place only. The rest of +an entry is what no code can state: owner, contact, licence, update frequency, +and the source fields deliberately left unpublished with the reason for each. +The field mapping stays in the source class, which is the only place that knows +the feed's shape. + +Field names follow [DCAT-AP], the metadata profile European data portals +harvest, so registering a data set is a translation of its entry rather than a +new survey. See [ADR 007](docs/adr/007-source-manifest.md). + +Adding a data set means adding one `SourceInterface` implementation and one +manifest entry. The class is discovered through +`#[AutoconfigureTag('app.source')]` and shows up as an `app:import` argument +with no further wiring. Design decisions are recorded in [docs/adr](docs/adr/README.md). [NGSI-LD]: https://www.etsi.org/committee/cim +[DCAT-AP]: https://semiceu.github.io/DCAT-AP/ ## Broker From 1d1e303562728098ad0dbf0470ba66a3368040cc Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 10:18:13 +0200 Subject: [PATCH 028/101] Cleaned up sources.yaml --- config/sources.yaml | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/config/sources.yaml b/config/sources.yaml index 8081a03..b011401 100644 --- a/config/sources.yaml +++ b/config/sources.yaml @@ -1,15 +1,5 @@ # The data sets this application publishes, one entry per source key. # -# The facts the import needs — where the feed is read from, the coordinate -# reference system it publishes, the Smart Data Model it is published as — are -# read from here, so they exist in exactly one place. -# -# The remaining fields record what no code can state: who owns the data, on -# what terms, how often it changes, and which source fields are deliberately -# not published. The rule is to record only what the code cannot tell you, so -# the field mapping itself stays in the source class, which is the only place -# that knows the feed's shape. -# # Field names follow DCAT-AP, the metadata profile European data portals # harvest, so registering a data set is a translation of its entry rather than # a new survey. See docs/adr/007-source-manifest.md. @@ -39,8 +29,8 @@ sources: # was left out, or why. omitted_fields: ident: 'Single-letter code; its meaning is not documented and not confirmed by the data owner.' - oprettet_af: 'Directory username of the municipal employee who created the record — personal data.' - rettet_af: 'Directory username of the municipal employee who last edited the record — personal data.' - oprettet_dato: 'Describes the register record rather than the parking bay.' - rettet_dato: 'Describes the register record rather than the parking bay. A candidate for observedAt, not yet mapped.' + oprettet_af: 'Directory username of the municipal employee who created the record.' + rettet_af: 'Directory username of the municipal employee who last edited the record.' + oprettet_dato: 'Describes the register record.' + rettet_dato: 'Describes the register record.' mi_style: 'MapInfo rendering style, empty throughout the export.' From fd623fc8511addaad9348a375f68c150643aacfa Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 10:25:03 +0200 Subject: [PATCH 029/101] Cleaned up ADR 007 --- docs/adr/007-source-manifest.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/adr/007-source-manifest.md b/docs/adr/007-source-manifest.md index 241eb8f..d644c8f 100644 --- a/docs/adr/007-source-manifest.md +++ b/docs/adr/007-source-manifest.md @@ -26,8 +26,7 @@ written description of the same value, which can then disagree. Recording only the first leaves the rest unwritten, and the questions it answers are then answered from memory. -This ADR serves to decide where a data set's own facts are recorded, and which -of them the code reads. +This ADR serves to decide where a data set's own facts are recorded. ### Drivers @@ -114,9 +113,6 @@ Rationale: - **A wrong reference system or model in the manifest is as damaging as a wrong one in code, while looking less like code.** A wrong reference system yields coordinates that are well-formed and in the wrong place. -- **Changing the published model changes entity identifiers.** Editing one - value can therefore orphan everything already published; see ADR 005 and ADR - 006. - **Fields no code reads have nothing keeping them current.** Until catalogue entities are generated from them, only review does. - Validating the record is work the environment did not require. From bfcac8753c818bb2bcc3cfcbc1fc47021440fde8 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 10:27:37 +0200 Subject: [PATCH 030/101] Shortened comment --- src/Source/MtmSpatialMaps/HandicapParking.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 72e0653..66a0c55 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -12,16 +12,7 @@ use App\Source\SourceInterface; /** - * Disabled parking bays in Aarhus Municipality, exported from SpatialMap. - * - * Where the feed is read from, the CRS it publishes and the model it is - * published as come from this key's manifest entry; this class owns only the - * field mapping. - * - * Published at site level rather than as a ParkingGroup subdivision: the feed - * describes locations with a count of reserved bays and nothing above them, - * and ParkingGroup requires a parent site this source does not contain. See - * ADR 006, and ADR 005 rule 2 for the principle behind it. + * Disabled parking bays in Aarhus Municipality * * @see config/sources.yaml * @see https://github.com/smart-data-models/dataModel.Parking/tree/master/OnStreetParking From f51f6111809ba0b03205370ab5ef0b50edfbd4b4 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 10:41:28 +0200 Subject: [PATCH 031/101] Updated changelog --- CHANGELOG.md | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5758a47..b089b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,36 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added - -- `app:import` command with `--dry-run` and `--limit`, exposed as `task import`. -- `SourceInterface`: extension point for further data sets, discovered through - `#[AutoconfigureTag('app.source')]`. -- `Wgs84Transformer`: reprojects coordinates from any registered CRS to WGS84, - for a single position or a whole GeoJSON geometry of any type. -- `FeedReader`: reads a feed from a filesystem path or an http(s) URL and decodes - it, without interpreting its shape. -- `NgsiEntity`: builds normalized NGSI-LD entities. -- `NgsiLdBroker`: idempotent batch upsert to an NGSI-LD context broker. -- `task broker:entities` for reading entities back out of the broker. -- Architecture Decision Records under `docs/adr`. -- Added test suite -- `config/sources.yaml`: one record per data set, holding the feed URL, its CRS - and the model it is published as alongside the metadata no code can state — - owner, contact, licence, update frequency, and the source fields deliberately - left unpublished with the reason for each. Field names follow DCAT-AP. -- `SourceCatalog` and `SourceDescriptor`: read and validate the manifest, - failing loudly on a missing or malformed record rather than defaulting. - -### Changed - -- Sources read their feed URL, CRS and model from `config/sources.yaml` instead - of holding them in an environment variable and a class constant. - -### Removed - -- `ENTER_MTM_SPATIALMAPS_HANDICAP_PARKING_SOURCE`, and with it the pattern of - one environment variable per data set. Where a feed is read from is a fact - about the data set, not about the environment. +* [#3](https://github.com/itk-dev/enter/pull/3) + * Import command that reads a geospatial feed, reprojects it to WGS84 and upserts it to an NGSI-LD broker. + * A committed record per data set — feed, CRS, model, DCAT-AP metadata. + * An extension point for adding data sets, a test suite, and architecture decision records. [Unreleased]: https://github.com/itk-dev/enter From e556c01f5e858053a354e5105e3a7419de28fe9c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 10:45:08 +0200 Subject: [PATCH 032/101] Updated README --- README.md | 166 +----------------------------------------------------- 1 file changed, 2 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 511ead3..6762905 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Run `task` to see what cool task are available. Running `ddev` can help with oth ## Adapter -Takes an Aarhus open-data set, converts it to [NGSI-LD], and upserts it into the +Takes an open-data set, converts it to [NGSI-LD], and upserts it into the context broker. ``` text @@ -26,18 +26,6 @@ source feed (JSON) → context broker ``` -| Class | Responsibility | -| ------------------------------------------- | ------------------------------------------------- | -| `App\Source\SourceInterface` | Contract for one input data set | -| `App\Source\SourceCatalog` | Reads the source manifest | -| `App\Source\SourceDescriptor` | One manifest entry: what a data set is and where | -| `App\Source\FeedReader` | Feed URL → decoded JSON | -| `App\Source\MtmSpatialMaps\HandicapParking` | Disabled parking bays → `OnStreetParking` | -| `App\Geo\Wgs84Transformer` | Any registered CRS → WGS84, any GeoJSON geometry | -| `App\Ngsi\NgsiEntity` | Builds normalized NGSI-LD entities | -| `App\Broker\NgsiLdBroker` | Batch upsert to the broker | -| `App\Command\ImportCommand` | `app:import` | - ``` shell task import # list the available sources task import -- mtm_spatialmaps-handicap-parking # import one @@ -48,17 +36,7 @@ task broker:entities -- OnStreetParking 10 # read back what land ### Source manifest Every data set is recorded in [config/sources.yaml](config/sources.yaml), keyed -by the identifier `app:import` takes as its argument. The feed URL, the -coordinate reference system it publishes and the Smart Data Model it is -published as are read from there, so each exists in one place only. The rest of -an entry is what no code can state: owner, contact, licence, update frequency, -and the source fields deliberately left unpublished with the reason for each. -The field mapping stays in the source class, which is the only place that knows -the feed's shape. - -Field names follow [DCAT-AP], the metadata profile European data portals -harvest, so registering a data set is a translation of its entry rather than a -new survey. See [ADR 007](docs/adr/007-source-manifest.md). +by the identifier `app:import` takes as its argument. See [ADR 007](docs/adr/007-source-manifest.md). Adding a data set means adding one `SourceInterface` implementation and one manifest entry. The class is discovered through @@ -77,143 +55,3 @@ A [Scorpio Broker](https://scorpio.readthedocs.io/) is part of the development s ``` shell ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/types | jq" ``` - -Load some example data: - -``` shell name=import-toilet -ddev console app:broker:entity:delete toilet -ddev console app:broker:import:geojson toilet 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter' -ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --get --data-urlencode type=toilet" | jq '.[]|with_entries(select([.key] | inside(["id", "type", "location"])))' -``` - -``` shell name=import-handicapparkering -ddev console app:broker:entity:delete handicapparkering -ddev console app:broker:import:geojson handicapparkering 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' -ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --get --data-urlencode type=handicapparkering" | jq '.[]|with_entries(select([.key] | inside(["id", "type", "location"])))' -``` - -``` shell name=import-hundeskov -ddev console app:broker:entity:delete hundeskov -ddev console app:broker:import:geojson hundeskov 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=hundeskove_friluftsliv_aarhus' -ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --get --data-urlencode type=hundeskov" | jq '.[]|with_entries(select([.key] | inside(["id", "type", "location", "geometry"])))' -``` - -### Broker API request examples - - - -``` shell name=scorpio-entity-create substitutions="{«entity-type»: Room, «entity-id»: 'house2:smartrooms:room1'}" -ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities --header 'content-type: application/json' --data @-" <<'JSON' -{ - "type": "«entity-type»", - "id": "«entity-id»" -} -JSON - - -# EPSG:4326?! -ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/entities/«entity-id»/attrs --header 'content-type: application/json' --data @-" <<'JSON' -{ - "location": { - "type": "geo:json", - "value": { - "type": "Point", - "coordinates": [ - 10.15711687080293, 56.126271111641266 - ] - } - } -} -JSON - - -# Get the entities -ddev exec "curl --silent --header 'accept: application/ld+json' http://scorpio.local:9090/ngsi-ld/v1/entities?type=«entity-type» | jq" - -# Get the entities as GeoJSON -ddev exec "curl --silent --header 'accept: application/geo+json' http://scorpio.local:9090/ngsi-ld/v1/entities?type=«entity-type» | jq" -``` - -> [!CAUTION] -> Excuse me what?! -> -> ``` shell -> ddev exec "curl --silent --header 'accept: application/geo+json' 'http://scorpio.local:9090/ngsi-ld/v1/entities?georel=near;maxDistance%3D%3D2000&geometry=Point&coordinates=%5B8,40%5D'" -> ddev exec "curl --silent --header 'accept: application/geo+json' 'http://scorpio.local:9090/ngsi-ld/v1/entities?georel=near;maxDistance==2000&geometry=Point&coordinates=%5B8,40%5D'" -> ddev exec "curl --silent --header 'accept: application/geo+json' 'http://scorpio.local:9090/ngsi-ld/v1/entities?georel=near;maxDistance==2000&geometry=Point&coordinates=[10,56]'" -> > ``` - -## GeoJSON - -* "[Position](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1)": `(longitude, latitude)` - * "[Coordinate Reference System](https://datatracker.ietf.org/doc/html/rfc7946#section-4)" - * But … - - : - - ```json - { - "type": "FeatureCollection", - "crs": { - "type": "name", - "properties": { - "name": "EPSG:25832" - } - }, - "bbox": [563262.0903961, 6209995.05346621, 579587.800694027, 6231954.36230685], - "features": [ - { - "type": "Feature", - "geometry": { - "type": "MultiPoint", - "coordinates": [ - [576933.018139537, 6218035.22691216] - ] - }, - "properties": { - … - ``` - -* - ---- - -* -* -* -* - -Must `location` be a `Point` in ngsi-ld? - -``` shell -ddev exec --service scorpio-db "psql ngb ngb" -``` - -``` sql -2026-08-30 10:26:19.789 UTC [41] LOG: execute 0000000: WITH D0 AS (SELECT ID, ENTITY, TRUE as PARENT FROM ENTITY WHERE ST_DWithin( location::geography, ST_SetSRID(ST_GeomFromGeoJSON('{"type": "Point", "coordinates": [10.236808047161853,56.101226991176155] }'), 4326)::geography, 2000.0) ORDER BY createdAt limit $1 offset $2) SELECT ID, ENTITY, PARENT FROM D0 -``` - - - -``` shell -ddev exec --service scorpio-db "psql ngb ngb" <<< "SELECT id , e_types, location FROM entity;" -``` - -``` shell name=hmm -ddev console app:import:geojson toilet 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter' \ - && ddev exec --service scorpio-db "psql ngb ngb" <<< "SELECT id , e_types, ST_AsText(location) AS location FROM entity WHERE id = 'toilet:0000';" \ - && ddev exec --service scorpio-db "psql ngb ngb" <<< "SELECT temporalentity_id, ST_AsText(location) AS location, ST_asText(geovalue) AS geovalue, createdat FROM temporalentityattrinstance WHERE temporalentity_id = 'toilet:0000' ORDER BY createdat DESC LIMIT 10;" -``` - - - -* [Public toilets in Aarhus Municipality](https://www.opendata.dk/city-of-aarhus/toiletter-i-aarhus-kommune) - * [Offentlige - toiletter](http://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=andre_toiletter): - -* [Parking in Aarhus Municipality – Zones, Permits and - Spaces](https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune) - * [Handicapparkering](https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap): - - -* From 8e0627b21acdae7542b27c59d54307b8faa59e2e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 10:46:16 +0200 Subject: [PATCH 033/101] Removed comment from Taskfile --- Taskfile.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 6ac1694..b5cfede 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -40,11 +40,6 @@ tasks: desc: 'Import a source into the broker, e.g. task import -- mtm_spatialmaps-handicap-parking' cmd: ddev console app:import {{.CLI_ARGS}} - # The broker itself needs no start task: Scorpio is a DDEV custom service - # (.ddev/docker-compose.scorpio.yaml), so it comes up with the rest of the - # site. Read requests need the domain context in a Link header, which the - # script supplies; the env values come from the dotenv block above, and are - # passed in explicitly because the container shell does not read .env. broker:entities: desc: 'List broker entities of a type, e.g. task broker:entities -- OnStreetParking 10' cmd: >- From c29945e4e383b5466c0d153d961608a0d7a0c3d9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 10:48:43 +0200 Subject: [PATCH 034/101] Fixed coding standards --- README.md | 1 - src/Source/MtmSpatialMaps/HandicapParking.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6762905..3957294 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,6 @@ with no further wiring. Design decisions are recorded in [docs/adr](docs/adr/README.md). [NGSI-LD]: https://www.etsi.org/committee/cim -[DCAT-AP]: https://semiceu.github.io/DCAT-AP/ ## Broker diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 66a0c55..bf7dead 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -12,7 +12,7 @@ use App\Source\SourceInterface; /** - * Disabled parking bays in Aarhus Municipality + * Disabled parking bays in Aarhus Municipality. * * @see config/sources.yaml * @see https://github.com/smart-data-models/dataModel.Parking/tree/master/OnStreetParking From 01cfeb5b227914bf6837440bd8ffae96299376b3 Mon Sep 17 00:00:00 2001 From: Jeppe Julius Krogh <106669866+jeppekroghitk@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:55:31 +0200 Subject: [PATCH 035/101] Update src/Geo/Wgs84Transformer.php Co-authored-by: Mikkel Ricky --- src/Geo/Wgs84Transformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Geo/Wgs84Transformer.php b/src/Geo/Wgs84Transformer.php index 4894555..3d46433 100644 --- a/src/Geo/Wgs84Transformer.php +++ b/src/Geo/Wgs84Transformer.php @@ -107,7 +107,7 @@ public function point(string $srid, float $x, float $y): array * * @return array{type: string, coordinates: mixed} */ - public function geometry(string $srid, array $geometry): array + public function transformGeometry(string $srid, array $geometry): array { $type = $geometry['type'] ?? null; From b94942983324a5f1942423f6f0fa2583a6922ac4 Mon Sep 17 00:00:00 2001 From: Jeppe Julius Krogh <106669866+jeppekroghitk@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:57:01 +0200 Subject: [PATCH 036/101] Update src/Geo/Wgs84Transformer.php Co-authored-by: Mikkel Ricky --- src/Geo/Wgs84Transformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Geo/Wgs84Transformer.php b/src/Geo/Wgs84Transformer.php index 3d46433..84b7593 100644 --- a/src/Geo/Wgs84Transformer.php +++ b/src/Geo/Wgs84Transformer.php @@ -129,7 +129,7 @@ public function transformGeometry(string $srid, array $geometry): array * * @return array */ - private function transformCoordinates(string $srid, mixed $coordinates): array + private function transformCoordinates(string $srid, array $coordinates): array { if (!\is_array($coordinates) || [] === $coordinates) { throw new \InvalidArgumentException('GeoJSON coordinates must be a non-empty array.'); From 684f13e31053478fc2ccaa433f102408bac3df02 Mon Sep 17 00:00:00 2001 From: Jeppe Julius Krogh <106669866+jeppekroghitk@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:23:26 +0200 Subject: [PATCH 037/101] Update src/Ngsi/NgsiEntity.php Co-authored-by: Mikkel Ricky --- src/Ngsi/NgsiEntity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ngsi/NgsiEntity.php b/src/Ngsi/NgsiEntity.php index 6f38391..dbc2f79 100644 --- a/src/Ngsi/NgsiEntity.php +++ b/src/Ngsi/NgsiEntity.php @@ -30,7 +30,7 @@ public function id(): string * because the source data uses "" for "not filled in" and a broker would * otherwise store the emptiness as a fact. */ - public function property(string $name, mixed $value, ?string $observedAt = null): self + public function setProperty(string $name, mixed $value, ?string $observedAt = null): self { if (null === $value || '' === $value || [] === $value) { return $this; From 43ef3ac7821fe542185cd9156100a0b8cdfdbd80 Mon Sep 17 00:00:00 2001 From: Jeppe Julius Krogh <106669866+jeppekroghitk@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:23:56 +0200 Subject: [PATCH 038/101] Update src/Source/SourceInterface.php Co-authored-by: Mikkel Ricky --- src/Source/SourceInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php index 978c5ce..b372d28 100644 --- a/src/Source/SourceInterface.php +++ b/src/Source/SourceInterface.php @@ -19,7 +19,7 @@ interface SourceInterface { /** - * Identifier used to select this source on the command line. + * Unique identifier for this source. */ public function key(): string; From 21d75592a1122f46a1647720e99579871f74707e Mon Sep 17 00:00:00 2001 From: Jeppe Julius Krogh <106669866+jeppekroghitk@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:26:46 +0200 Subject: [PATCH 039/101] Update src/Source/FeedReader.php Co-authored-by: Mikkel Ricky --- src/Source/FeedReader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Source/FeedReader.php b/src/Source/FeedReader.php index 6a9c280..d2b6400 100644 --- a/src/Source/FeedReader.php +++ b/src/Source/FeedReader.php @@ -9,7 +9,7 @@ /** * Fetches a feed over HTTP and decodes it. */ -final readonly class FeedReader +final readonly class DataSourceReader { public function __construct( private HttpClientInterface $client, From 93897e33ae0f1ac374a5325a52eb2e5190f06b2c Mon Sep 17 00:00:00 2001 From: Jeppe Julius Krogh <106669866+jeppekroghitk@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:28:32 +0200 Subject: [PATCH 040/101] Update src/Source/FeedReader.php Co-authored-by: Mikkel Ricky --- src/Source/FeedReader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Source/FeedReader.php b/src/Source/FeedReader.php index d2b6400..c84ce03 100644 --- a/src/Source/FeedReader.php +++ b/src/Source/FeedReader.php @@ -23,7 +23,7 @@ public function __construct( * be fetched, does not contain valid JSON, or * does not decode to an array */ - public function read(string $location): array + public function read(string $url): array { if (!str_starts_with($location, 'http://') && !str_starts_with($location, 'https://')) { throw new \RuntimeException(\sprintf('Feed location must be an http(s) URL, got "%s".', $location)); From 2a56d2ca3cb2122e0c3d18f20eba7db4a7a6bea4 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 14:34:56 +0200 Subject: [PATCH 041/101] Renamed FeedReader to DataSourceReader --- src/Source/{FeedReader.php => DataSourceReader.php} | 0 src/Source/MtmSpatialMaps/HandicapParking.php | 4 ++-- .../{FeedReaderTest.php => DataSourceReaderTest.php} | 8 ++++---- tests/Source/MtmSpatialMaps/HandicapParkingTest.php | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/Source/{FeedReader.php => DataSourceReader.php} (100%) rename tests/Source/{FeedReaderTest.php => DataSourceReaderTest.php} (94%) diff --git a/src/Source/FeedReader.php b/src/Source/DataSourceReader.php similarity index 100% rename from src/Source/FeedReader.php rename to src/Source/DataSourceReader.php diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index bf7dead..441a452 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -6,7 +6,7 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; -use App\Source\FeedReader; +use App\Source\DataSourceReader; use App\Source\SourceCatalog; use App\Source\SourceDescriptor; use App\Source\SourceInterface; @@ -22,7 +22,7 @@ private const string KEY = 'mtm_spatialmaps-handicap-parking'; public function __construct( - private FeedReader $reader, + private DataSourceReader $reader, private Wgs84Transformer $transformer, private SourceCatalog $catalog, ) { diff --git a/tests/Source/FeedReaderTest.php b/tests/Source/DataSourceReaderTest.php similarity index 94% rename from tests/Source/FeedReaderTest.php rename to tests/Source/DataSourceReaderTest.php index 8d925b8..9c866ca 100644 --- a/tests/Source/FeedReaderTest.php +++ b/tests/Source/DataSourceReaderTest.php @@ -4,12 +4,12 @@ namespace App\Tests\Source; -use App\Source\FeedReader; +use App\Source\DataSourceReader; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; -class FeedReaderTest extends TestCase +class DataSourceReaderTest extends TestCase { /** * Content-agnostic on purpose: a minimal FeatureCollection with meaningless @@ -18,9 +18,9 @@ class FeedReaderTest extends TestCase */ private const string FEATURE_COLLECTION = '{"type":"FeatureCollection","features":[{"example":1},{"example":2}]}'; - private function reader(?MockHttpClient $client = null): FeedReader + private function reader(?MockHttpClient $client = null): DataSourceReader { - return new FeedReader($client ?? new MockHttpClient()); + return new DataSourceReader($client ?? new MockHttpClient()); } public function testItReadsFromAnHttpUrl(): void diff --git a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php index e0065a7..4a67b14 100644 --- a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php +++ b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php @@ -6,7 +6,7 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; -use App\Source\FeedReader; +use App\Source\DataSourceReader; use App\Source\MtmSpatialMaps\HandicapParking; use App\Source\SourceCatalog; use App\Source\SourceDescriptor; @@ -40,7 +40,7 @@ protected function setUp(): void return new MockResponse(json_encode($this->feed(), \JSON_THROW_ON_ERROR)); }); - $source = new HandicapParking(new FeedReader($client), new Wgs84Transformer(), $catalog); + $source = new HandicapParking(new DataSourceReader($client), new Wgs84Transformer(), $catalog); $this->entities = array_map( static fn (NgsiEntity $entity): array => $entity->toArray(['https://example.com/context.jsonld']), From e4452fd003532f507c34816b785a529f461da2ef Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 14:47:03 +0200 Subject: [PATCH 042/101] Validated the source manifest as Symfony config Replaces the hand-written field checks with a config tree, and reads the whole manifest during container warm-up, so a malformed entry fails the build instead of the one import that happens to select it. --- CHANGELOG.md | 2 + composer.json | 1 + composer.lock | 2 +- src/Source/SourceCatalog.php | 136 +++++++-------------- src/Source/SourceCatalogWarmer.php | 38 ++++++ src/Source/SourceManifestConfiguration.php | 115 +++++++++++++++++ tests/Source/SourceCatalogTest.php | 91 +++++++++++++- tests/Source/SourceCatalogWarmerTest.php | 86 +++++++++++++ 8 files changed, 376 insertions(+), 95 deletions(-) create mode 100644 src/Source/SourceCatalogWarmer.php create mode 100644 src/Source/SourceManifestConfiguration.php create mode 100644 tests/Source/SourceCatalogWarmerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b089b68..e6e1c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,5 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Import command that reads a geospatial feed, reprojects it to WGS84 and upserts it to an NGSI-LD broker. * A committed record per data set — feed, CRS, model, DCAT-AP metadata. * An extension point for adding data sets, a test suite, and architecture decision records. + * Source manifest validated against a Symfony config tree and read during container warm-up, so a malformed + entry fails the build rather than the one import that selects it. [Unreleased]: https://github.com/itk-dev/enter diff --git a/composer.json b/composer.json index b3bcf60..fad8d55 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "proj4php/proj4php": "^2.0", "symfony/asset": "~8.1.0", "symfony/asset-mapper": "~8.1.0", + "symfony/config": "~8.1.0", "symfony/console": "~8.1.6", "symfony/dotenv": "~8.1.6", "symfony/flex": "^2", diff --git a/composer.lock b/composer.lock index 2b3d45f..2d8ddb4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "89753fcad5de669970a5e88087a1a523", + "content-hash": "dfaceee7643a3761619ee1e8ae943e6a", "packages": [ { "name": "composer/semver", diff --git a/src/Source/SourceCatalog.php b/src/Source/SourceCatalog.php index cdcd9f9..2b385df 100644 --- a/src/Source/SourceCatalog.php +++ b/src/Source/SourceCatalog.php @@ -4,6 +4,8 @@ namespace App\Source; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Processor; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; @@ -11,10 +13,13 @@ /** * The manifest of data sets this application publishes. * - * Parsed on first use rather than during container warm-up, so a malformed - * entry fails the import that needs it instead of every cache clear. + * The record's shape is a Symfony config tree rather than hand-written checks, + * and SourceCatalogWarmer reads the whole manifest during container warm-up, so + * a malformed entry fails the build instead of waiting for the one import that + * happens to select it. * * @see config/sources.yaml + * @see SourceManifestConfiguration * @see docs/adr/007-source-manifest.md */ final class SourceCatalog @@ -57,113 +62,60 @@ public function all(): array */ private function load(): array { - if (!is_file($this->manifest)) { - throw new \RuntimeException(\sprintf('Source manifest "%s" does not exist.', $this->manifest)); - } - - try { - $parsed = Yaml::parseFile($this->manifest); - } catch (ParseException $exception) { - throw new \RuntimeException(\sprintf('Source manifest "%s" is not valid YAML: %s', $this->manifest, $exception->getMessage()), previous: $exception); - } - - // Without this the wrong shape yields an empty catalogue, which reads - // as "no data sets are registered" rather than as a broken file. - $sources = \is_array($parsed) ? $parsed['sources'] ?? null : null; - if (!\is_array($sources)) { - throw new \RuntimeException(\sprintf('Source manifest "%s" must contain a "sources" mapping at the top level.', $this->manifest)); - } - $descriptors = []; - foreach ($sources as $key => $entry) { - $key = (string) $key; - if (!\is_array($entry)) { - throw new \RuntimeException(\sprintf('Entry "%s" in %s must be a mapping, got %s.', $key, $this->manifest, get_debug_type($entry))); - } - - $descriptors[$key] = $this->descriptor($key, $entry); + foreach ($this->validated() as $key => $entry) { + $descriptors[$key] = new SourceDescriptor( + key: $key, + title: $entry['title'], + accessUrl: $entry['access_url'], + crs: $entry['crs'], + model: $entry['model'], + description: $entry['description'], + publisher: $entry['publisher'], + contact: $entry['contact'], + landingPage: $entry['landing_page'], + mediaType: $entry['media_type'], + updateFrequency: $entry['update_frequency'], + licence: $entry['licence'], + omittedFields: $entry['omitted_fields'], + ); } return $descriptors; } /** - * @param array $entry - */ - private function descriptor(string $key, array $entry): SourceDescriptor - { - return new SourceDescriptor( - key: $key, - title: $this->required($key, $entry, 'title'), - accessUrl: $this->required($key, $entry, 'access_url'), - crs: $this->required($key, $entry, 'crs'), - model: $this->required($key, $entry, 'model'), - description: $this->optional($key, $entry, 'description'), - publisher: $this->optional($key, $entry, 'publisher'), - contact: $this->optional($key, $entry, 'contact'), - landingPage: $this->optional($key, $entry, 'landing_page'), - mediaType: $this->optional($key, $entry, 'media_type'), - updateFrequency: $this->optional($key, $entry, 'update_frequency'), - licence: $this->optional($key, $entry, 'licence'), - omittedFields: $this->omittedFields($key, $entry), - ); - } - - /** - * @param array $entry - */ - private function required(string $key, array $entry, string $field): string - { - return $this->optional($key, $entry, $field) - ?? throw new \RuntimeException(\sprintf('Entry "%s" in %s is missing the required field "%s"; an import cannot run without it.', $key, $this->manifest, $field)); - } - - /** - * An empty value means "not filled in", the same as an absent key, so both - * become null rather than an empty string. - * - * @param array $entry + * @return array}> */ - private function optional(string $key, array $entry, string $field): ?string + private function validated(): array { - $value = $entry[$field] ?? null; - - if (null === $value || '' === $value) { - return null; + if (!is_file($this->manifest)) { + throw new \RuntimeException(\sprintf('Source manifest "%s" does not exist.', $this->manifest)); } - if (!is_scalar($value)) { - throw new \RuntimeException(\sprintf('Field "%s" of entry "%s" in %s must be a single value, got %s.', $field, $key, $this->manifest, get_debug_type($value))); + try { + $parsed = Yaml::parseFile($this->manifest); + } catch (ParseException $exception) { + throw new \RuntimeException(\sprintf('Source manifest "%s" is not valid YAML: %s', $this->manifest, $exception->getMessage()), previous: $exception); } - return trim((string) $value); - } - - /** - * @param array $entry - * - * @return array - */ - private function omittedFields(string $key, array $entry): array - { - $omitted = $entry['omitted_fields'] ?? []; - - if (!\is_array($omitted)) { - throw new \RuntimeException(\sprintf('Field "omitted_fields" of entry "%s" in %s must map each field to the reason it is not published.', $key, $this->manifest)); + // The tree is rooted at the entries themselves, so it never sees the + // key holding them. Without this the wrong shape yields an empty + // catalogue, which reads as "no data sets are registered" rather than + // as a broken file. + $sources = \is_array($parsed) ? $parsed['sources'] ?? null : null; + if (!\is_array($sources)) { + throw new \RuntimeException(\sprintf('Source manifest "%s" must contain a "sources" mapping at the top level.', $this->manifest)); } - $reasons = []; - foreach ($omitted as $field => $reason) { - // The reason is the half of the record that cannot be recovered - // from the code, so a bare list of names is not accepted. - if (!\is_string($reason) || '' === trim($reason)) { - throw new \RuntimeException(\sprintf('Omitted field "%s" of entry "%s" in %s needs a reason.', $field, $key, $this->manifest)); - } - - $reasons[(string) $field] = trim($reason); + try { + /** @var array}> $processed */ + $processed = new Processor()->processConfiguration(new SourceManifestConfiguration(), [$sources]); + } catch (InvalidConfigurationException $exception) { + throw new \RuntimeException(\sprintf('Source manifest "%s" is invalid: %s', $this->manifest, $exception->getMessage()), previous: $exception); } - return $reasons; + return $processed; } } diff --git a/src/Source/SourceCatalogWarmer.php b/src/Source/SourceCatalogWarmer.php new file mode 100644 index 0000000..c262f5d --- /dev/null +++ b/src/Source/SourceCatalogWarmer.php @@ -0,0 +1,38 @@ +catalog->all(); + + return []; + } +} diff --git a/src/Source/SourceManifestConfiguration.php b/src/Source/SourceManifestConfiguration.php new file mode 100644 index 0000000..05383d6 --- /dev/null +++ b/src/Source/SourceManifestConfiguration.php @@ -0,0 +1,115 @@ +getRootNode() + // The import selects a data set by its key exactly as written in + // the manifest. Key normalization rewrites a key that contains + // dashes and no underscore, which breaks that lookup silently. + ->normalizeKeys(false) + ->requiresAtLeastOneElement() + ->arrayPrototype() + ->beforeNormalization() + ->always(self::blankToNull()) + ->end() + ->children() + ->scalarNode('title') + ->isRequired() + ->cannotBeEmpty() + ->info('What the data set is called where it is published.') + ->end() + ->scalarNode('access_url') + ->isRequired() + ->cannotBeEmpty() + ->info('Where the feed is read from; an import cannot run without it.') + ->end() + ->scalarNode('crs') + ->isRequired() + ->cannotBeEmpty() + ->info('The CRS the feed publishes coordinates in, e.g. "EPSG:25832". A wrong value yields well-formed coordinates in the wrong place.') + ->end() + ->scalarNode('model') + ->isRequired() + ->cannotBeEmpty() + ->info('Smart Data Model the data set is published as.') + ->end() + ->scalarNode('description')->defaultNull()->end() + ->scalarNode('publisher')->defaultNull()->end() + ->scalarNode('contact')->defaultNull()->end() + ->scalarNode('landing_page')->defaultNull()->end() + ->scalarNode('media_type')->defaultNull()->end() + ->scalarNode('update_frequency')->defaultNull()->end() + ->scalarNode('licence') + ->defaultNull() + ->info('Empty means the terms are unsettled; DCAT-AP requires one before the data set can be registered.') + ->end() + ->arrayNode('omitted_fields') + ->defaultValue([]) + ->normalizeKeys(false) + ->scalarPrototype() + ->beforeNormalization() + ->always(self::blankToNullValue()) + ->end() + ->cannotBeEmpty() + ->info('The reason a field is withheld cannot be recovered from the code, so a bare list of names is not accepted.') + ->end() + ->end() + ->end() + ->end(); + + return $tree; + } + + /** + * An empty value means "not filled in", the same as an absent key, so both + * become null rather than an empty string. + * + * Normalization runs before the type check, so an entry that is not a + * mapping has to pass through untouched for the tree to report it as one. + */ + private static function blankToNull(): \Closure + { + return static function (mixed $entry): mixed { + if (!\is_array($entry)) { + return $entry; + } + + return array_map(self::blankToNullValue(), $entry); + }; + } + + private static function blankToNullValue(): \Closure + { + return static function (mixed $value): mixed { + if (!\is_string($value)) { + return $value; + } + + $value = trim($value); + + return '' === $value ? null : $value; + }; + } +} diff --git a/tests/Source/SourceCatalogTest.php b/tests/Source/SourceCatalogTest.php index e5f1dbd..61a0f57 100644 --- a/tests/Source/SourceCatalogTest.php +++ b/tests/Source/SourceCatalogTest.php @@ -84,7 +84,7 @@ public function testItRejectsAnEntryMissingAFieldTheImportNeeds(): void YAML)); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('missing the required field "crs"'); + $this->expectExceptionMessage('The child config "crs" under "sources.a-source" must be configured'); $catalog->all(); } @@ -103,11 +103,98 @@ public function testItRejectsAnOmittedFieldWithoutAReason(): void YAML)); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('needs a reason'); + $this->expectExceptionMessage('"sources.a-source.omitted_fields.some_field" cannot contain an empty value'); $catalog->all(); } + /** + * The import selects a data set by the key written in the manifest, and the + * config tree rewrites a key that has dashes and no underscore unless told + * otherwise. A rewritten key stops matching without saying so. + */ + public function testItKeepsADashedSourceKeyIntact(): void + { + $catalog = new SourceCatalog($this->manifest(<<<'YAML' + sources: + handicap-parking: + title: A source + access_url: https://example.com/feed.json + crs: EPSG:25832 + model: Example + YAML)); + + $this->assertSame(['handicap-parking'], array_keys($catalog->all())); + $this->assertSame('handicap-parking', $catalog->get('handicap-parking')->key); + } + + /** + * A misspelled optional field was dropped in silence before, which loses a + * fact the record exists to carry. + */ + public function testItRejectsAFieldTheManifestDoesNotDefine(): void + { + $catalog = new SourceCatalog($this->manifest(<<<'YAML' + sources: + a-source: + title: A source + access_url: https://example.com/feed.json + crs: EPSG:25832 + model: Example + license: CC-BY-4.0 + YAML)); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unrecognized option "license" under "sources.a-source"'); + + $catalog->all(); + } + + public function testItRejectsAnEntryThatIsNotAMapping(): void + { + $catalog = new SourceCatalog($this->manifest("sources:\n a-source: just a string\n")); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid type for path "sources.a-source"'); + + $catalog->all(); + } + + public function testItRejectsAManifestThatRegistersNothing(): void + { + $catalog = new SourceCatalog($this->manifest("sources: {}\n")); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('should have at least 1 element'); + + $catalog->all(); + } + + /** + * An empty value means "not filled in", the same as an absent key, so an + * unanswered question reads the same either way. + */ + public function testItReadsABlankOptionalFieldAsUnknown(): void + { + $catalog = new SourceCatalog($this->manifest(<<<'YAML' + sources: + a-source: + title: A source + access_url: https://example.com/feed.json + crs: EPSG:25832 + model: Example + publisher: ' Aarhus Kommune ' + contact: '' + licence: ~ + YAML)); + + $descriptor = $catalog->get('a-source'); + + $this->assertSame('Aarhus Kommune', $descriptor->publisher); + $this->assertNull($descriptor->contact); + $this->assertNull($descriptor->licence); + } + public function testItReportsAManifestThatIsNotThere(): void { $catalog = new SourceCatalog('/no/such/sources.yaml'); diff --git a/tests/Source/SourceCatalogWarmerTest.php b/tests/Source/SourceCatalogWarmerTest.php new file mode 100644 index 0000000..02294d7 --- /dev/null +++ b/tests/Source/SourceCatalogWarmerTest.php @@ -0,0 +1,86 @@ + */ + private array $written = []; + + protected function tearDown(): void + { + foreach ($this->written as $path) { + if (is_file($path)) { + unlink($path); + } + } + + $this->written = []; + } + + /** + * An optional warmer can be skipped, and a manifest that is only read when + * an import selects it is exactly what warming exists to avoid. + */ + public function testItIsNotOptional(): void + { + $this->assertFalse($this->warmer(\dirname(__DIR__, 2).'/config/sources.yaml')->isOptional()); + } + + public function testItWarmsTheShippedManifestWithoutPreloadingAnything(): void + { + $warmer = $this->warmer(\dirname(__DIR__, 2).'/config/sources.yaml'); + + $this->assertSame([], $warmer->warmUp(sys_get_temp_dir(), sys_get_temp_dir())); + } + + /** + * The reason for warming at all: an entry no import selects still fails the + * build rather than waiting to be discovered. + */ + public function testItFailsOnAnEntryNoImportWouldReach(): void + { + $warmer = $this->warmer($this->manifest(<<<'YAML' + sources: + a-source: + title: A source + access_url: https://example.com/feed.json + crs: EPSG:25832 + model: Example + unreached-source: + title: Another source + access_url: https://example.com/other.json + model: Example + YAML)); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The child config "crs" under "sources.unreached-source" must be configured'); + + $warmer->warmUp(sys_get_temp_dir(), sys_get_temp_dir()); + } + + private function warmer(string $manifest): SourceCatalogWarmer + { + return new SourceCatalogWarmer(new SourceCatalog($manifest)); + } + + private function manifest(string $yaml): string + { + $path = tempnam(sys_get_temp_dir(), 'sources-'); + + if (false === $path) { + $this->fail('Could not create a temporary manifest.'); + } + + file_put_contents($path, $yaml); + $this->written[] = $path; + + return $path; + } +} From 02cc6cf683bcb7de4ae9e1db665ef56c385c63a2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 14:52:07 +0200 Subject: [PATCH 043/101] Fixed error in pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e39b258..333ba2f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,3 @@ - Date: Mon, 7 Sep 2026 15:42:52 +0200 Subject: [PATCH 044/101] Corrected adr 007 --- docs/adr/007-source-manifest.md | 128 ++++++++++---------------------- 1 file changed, 41 insertions(+), 87 deletions(-) diff --git a/docs/adr/007-source-manifest.md b/docs/adr/007-source-manifest.md index d644c8f..7491f71 100644 --- a/docs/adr/007-source-manifest.md +++ b/docs/adr/007-source-manifest.md @@ -10,109 +10,63 @@ ## Context -Each published data set carries facts the import needs — where the feed is read -from, the coordinate reference system its coordinates are in, and the model it -is published as — and facts only people need: who owns the data, on what terms -it may be republished, how often it changes, and which of its fields are -deliberately not published, with the reason for each. +This application publishes data sets it does not own. Each needs a +description — where it comes from, how to read it, who owns it, on what terms +it may be republished. -The first group must be readable by code. The second is what a public data -portal requires at registration, and what a data owner asks for when -establishing what happened to their data. Both grow with the number of data -sets. - -Recording the two groups separately produces a machine-readable value and a -written description of the same value, which can then disagree. Recording only -the first leaves the rest unwritten, and the questions it answers are then -answered from memory. - -This ADR serves to decide where a data set's own facts are recorded. +This ADR serves to decide where the data sets and their descriptions live. ### Drivers -- **Functional:** the code reads the facts it needs from the same record a - person reads; a data set is registerable on a public portal without a fresh - survey; an incomplete record fails the import that needs it rather than - producing an incomplete publication. -- **Non-functional:** adding a data set requires no deployment change; the - record is reviewable as a diff; no fact exists in two places. +- **Functional:** each data set is listed with what is needed to read and + republish it. +- **Non-functional:** adding a data set needs no deployment change, and its + description is reviewable as a diff. ### Options Considered -1. **One environment variable per data set.** Follows the convention that - configuration belongs in the environment, and lets a value differ per - environment. But variable names grow with the catalogue, so each new data - set becomes a deployment change; the environment carries strings only, - leaving metadata beyond an address nowhere to live; and values are invisible - in review, so a wrong one is found by running the import. -2. **Every fact in the class that maps the data set.** Nothing can diverge, - there being one copy, and the language enforces its presence. But metadata - is then readable only by opening code, extracting it for portal registration - requires writing an extractor, and correcting a licence or a contact becomes - a code change reviewed as one. -3. **A committed manifest the code reads.** One record per data set, keyed by - the identifier the import selects it with, holding the facts the code needs - beside those it does not, in a shape a catalogue profile can be generated - from. Validating the record becomes work of our own, and the values are - identical in every environment. +1. **One environment variable per data set.** Variable names grow with the + catalogue, and the environment holds only strings. +2. **Every fact in the class that maps the data set.** Nothing can diverge, but + the description is readable only by opening code, and correcting a licence + becomes a code change. +3. **A committed manifest the code reads.** One record per data set, holding + the description beside the values the import needs; its shape has to be + declared. 4. **An external catalogue or registry service.** The eventual home of - published metadata, with search and harvesting already built. But it has to - be running for an import to work, it is a second system to operate, and it - must be populated before anything can be published from it — from records - that would have to live somewhere else in the meantime. + published metadata, but a second system to operate, populated before + anything can be published from it. ## Decision -Record each data set in a **committed manifest**, keyed by the identifier the -import selects it with, and read from it every fact the code needs. - -Four rules follow: - -1. **Record only what the code cannot state.** How a feed's fields map onto the - model, and every quirk of its shape, stay in the class that performs the - mapping. Restating them in the manifest recreates the divergence the - manifest exists to prevent. -2. **A fact both the code and a reader need is read from the manifest.** It is - not also written in code, in a comment, or in the README. -3. **An incomplete or malformed record is an error.** Fields an import cannot - run without are required, and their absence raises rather than defaulting. A - fact that is unknown is recorded as unknown, so the gap stays visible. -4. **Name the fields after the catalogue profile the data will be registered - under** — DCAT-AP — so that publication is a translation rather than a - redesign. - -Rationale: - -- What has to be prevented is a value diverging from its description, so the - two belong to the same record. -- Portal registration and answering a data owner need the same fields, and - neither can be derived from mapping code. -- A record is reviewed alongside the class that consumes it, so a reviewer sees - the address, the reference system and the model together with the mapping - that assumes them. -- Where a feed is read from is a fact about the data set, not about the machine - running the import, so the environment is the wrong place for it. +Keep one **committed manifest** listing every data set this application +publishes, keyed by the identifier the import selects a data set by, and read +from it every fact the code needs. + +1. **Record only what the code cannot state.** Field mappings and feed quirks + stay in the class that maps the data set. +2. **A fact the manifest records is not restated elsewhere**, in code, a + comment, or documentation. +3. **An incomplete record is an error.** Required fields raise rather than + default, an unknown fact is recorded as unknown, and the shape is a schema + the framework validates when the application is built. +4. **Name the fields after DCAT-AP**, the profile the data is registered under, + so publication is a translation. ## Consequences ### Positive -- One record per data set, reviewable as a diff and versioned with the code. -- Adding a data set is one class and one record, with no deployment change. -- Registration on a public portal is a translation of records that exist. -- Licence, ownership and what was withheld have a single answer, and an - unanswered question shows as an empty value rather than as nothing at all. -- The same records can generate catalogue entities later without introducing a - second source of truth. +- Every data set is listed in one place, reviewable as a diff and versioned + with the code. +- Portal registration and owner questions translate records that already exist. +- A wrong record, or a field the manifest does not define, fails the build. ### Negative / Trade-offs -- **Values are identical in every environment.** Pointing a data set at a copy - for testing means editing a committed file. That is consistent with reading - feeds where they live, but it removes an escape hatch the environment offered. -- **A wrong reference system or model in the manifest is as damaging as a wrong - one in code, while looking less like code.** A wrong reference system yields - coordinates that are well-formed and in the wrong place. -- **Fields no code reads have nothing keeping them current.** Until catalogue - entities are generated from them, only review does. -- Validating the record is work the environment did not require. +- Values are identical in every environment, so pointing a data set at a test + copy means editing a committed file. +- A wrong reference system looks less like code than it is, and yields + coordinates that are well-formed and misplaced. +- A malformed record blocks every build, not just the import that reads it. +- Fields no code reads have only review keeping them current. From 538ae480723af9d2184a460fb49ed6bd97004c82 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 15:48:02 +0200 Subject: [PATCH 045/101] ADR 007 edits --- docs/adr/007-source-manifest.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/adr/007-source-manifest.md b/docs/adr/007-source-manifest.md index 7491f71..47fcea2 100644 --- a/docs/adr/007-source-manifest.md +++ b/docs/adr/007-source-manifest.md @@ -10,31 +10,28 @@ ## Context -This application publishes data sets it does not own. Each needs a -description — where it comes from, how to read it, who owns it, on what terms -it may be republished. - -This ADR serves to decide where the data sets and their descriptions live. +This application publishes data sets it does not own. This ADR serves +to decide where the data sets and their specifications live. ### Drivers - **Functional:** each data set is listed with what is needed to read and republish it. - **Non-functional:** adding a data set needs no deployment change, and its - description is reviewable as a diff. + specification is reviewable as a diff. ### Options Considered 1. **One environment variable per data set.** Variable names grow with the - catalogue, and the environment holds only strings. + collection, and the environment holds only strings. 2. **Every fact in the class that maps the data set.** Nothing can diverge, but - the description is readable only by opening code, and correcting a licence + the specification is readable only by opening code, and correcting a licence becomes a code change. 3. **A committed manifest the code reads.** One record per data set, holding - the description beside the values the import needs; its shape has to be + the specification beside the values the import needs; its shape has to be declared. 4. **An external catalogue or registry service.** The eventual home of - published metadata, but a second system to operate, populated before + published specifications, but a second system to operate, populated before anything can be published from it. ## Decision @@ -50,8 +47,8 @@ from it every fact the code needs. 3. **An incomplete record is an error.** Required fields raise rather than default, an unknown fact is recorded as unknown, and the shape is a schema the framework validates when the application is built. -4. **Name the fields after DCAT-AP**, the profile the data is registered under, - so publication is a translation. +4. **Structure each record after DCAT-AP**, the specification the data will be + published under, so publication is a translation rather than a redesign. ## Consequences @@ -59,7 +56,8 @@ from it every fact the code needs. - Every data set is listed in one place, reviewable as a diff and versioned with the code. -- Portal registration and owner questions translate records that already exist. +- Publishing a specification onward, and answering the data owner, translate + records that already exist. - A wrong record, or a field the manifest does not define, fails the build. ### Negative / Trade-offs From cda1ba2f26b6e94d51f6b9d35a26c6c5692962b0 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 15:48:51 +0200 Subject: [PATCH 046/101] Raname calls to renamed methods --- src/Geo/Wgs84Transformer.php | 4 +++- src/Source/DataSourceReader.php | 10 +++++----- src/Source/MtmSpatialMaps/HandicapParking.php | 12 ++++++------ tests/Command/ImportCommandTest.php | 2 +- tests/Geo/Wgs84TransformerTest.php | 14 +++++++------- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Geo/Wgs84Transformer.php b/src/Geo/Wgs84Transformer.php index 84b7593..9f57abe 100644 --- a/src/Geo/Wgs84Transformer.php +++ b/src/Geo/Wgs84Transformer.php @@ -127,11 +127,13 @@ public function transformGeometry(string $srid, array $geometry): array * of those for Polygon, and so on. Recursing until the first element is * numeric handles every depth without enumerating the types. * + * @param array $coordinates + * * @return array */ private function transformCoordinates(string $srid, array $coordinates): array { - if (!\is_array($coordinates) || [] === $coordinates) { + if ([] === $coordinates) { throw new \InvalidArgumentException('GeoJSON coordinates must be a non-empty array.'); } diff --git a/src/Source/DataSourceReader.php b/src/Source/DataSourceReader.php index c84ce03..c8f3751 100644 --- a/src/Source/DataSourceReader.php +++ b/src/Source/DataSourceReader.php @@ -25,23 +25,23 @@ public function __construct( */ public function read(string $url): array { - if (!str_starts_with($location, 'http://') && !str_starts_with($location, 'https://')) { - throw new \RuntimeException(\sprintf('Feed location must be an http(s) URL, got "%s".', $location)); + if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) { + throw new \RuntimeException(\sprintf('Feed location must be an http(s) URL, got "%s".', $url)); } - $json = $this->fetch($location); + $json = $this->fetch($url); try { $decoded = json_decode($json, true, 512, \JSON_THROW_ON_ERROR); } catch (\JsonException $exception) { - throw new \RuntimeException(\sprintf('Invalid JSON in "%s": %s', $location, $exception->getMessage()), previous: $exception); + throw new \RuntimeException(\sprintf('Invalid JSON in "%s": %s', $url, $exception->getMessage()), previous: $exception); } // A JSON document may legally be a scalar. Every feed we consume is a // list or an object, and a scalar here means the location is wrong // rather than that the feed is empty. if (!\is_array($decoded)) { - throw new \RuntimeException(\sprintf('Expected a JSON array or object in "%s", got %s.', $location, get_debug_type($decoded))); + throw new \RuntimeException(\sprintf('Expected a JSON array or object in "%s", got %s.', $url, get_debug_type($decoded))); } return $decoded; diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 441a452..27bcdcd 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -78,12 +78,12 @@ private function toEntity(array $feature, SourceDescriptor $source): ?NgsiEntity // models have separate category enums, so values are not interchangeable. // `onStreet` is dropped because the entity type already states it. return $entity - ->property('name', $this->address($row)) - ->property('description', trim((string) ($row['bemrk'] ?? ''))) - ->property('category', ['forDisabled']) - ->property('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) - ->property('source', $source->accessUrl) - ->geoProperty('location', $this->transformer->geometry($source->crs, $geometry)); + ->setProperty('name', $this->address($row)) + ->setProperty('description', trim((string) ($row['bemrk'] ?? ''))) + ->setProperty('category', ['forDisabled']) + ->setProperty('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) + ->setProperty('source', $source->accessUrl) + ->geoProperty('location', $this->transformer->transformGeometry($source->crs, $geometry)); } /** diff --git a/tests/Command/ImportCommandTest.php b/tests/Command/ImportCommandTest.php index ae858a3..a1d68ad 100644 --- a/tests/Command/ImportCommandTest.php +++ b/tests/Command/ImportCommandTest.php @@ -121,7 +121,7 @@ public function testItRejectsAnUnknownSource(): void public function testDryRunPrintsThePayloadAndSendsNothing(): void { $entity = new NgsiEntity('urn:ngsi-ld:Example:1', 'Example') - ->property('name', 'Example'); + ->setProperty('name', 'Example'); $tester = $this->tester([$this->source('one-entity', $entity)]); diff --git a/tests/Geo/Wgs84TransformerTest.php b/tests/Geo/Wgs84TransformerTest.php index aacf039..2f2192b 100644 --- a/tests/Geo/Wgs84TransformerTest.php +++ b/tests/Geo/Wgs84TransformerTest.php @@ -93,7 +93,7 @@ public function testItAcceptsAdditionalDefinitions(): void public function testItReprojectsAPointGeometry(): void { - $geometry = $this->transformer->geometry(self::UTM32, [ + $geometry = $this->transformer->transformGeometry(self::UTM32, [ 'type' => 'Point', 'coordinates' => [self::REFERENCE_EASTING, self::REFERENCE_NORTHING], ]); @@ -104,7 +104,7 @@ public function testItReprojectsAPointGeometry(): void public function testItReprojectsALineString(): void { - $geometry = $this->transformer->geometry(self::UTM32, [ + $geometry = $this->transformer->transformGeometry(self::UTM32, [ 'type' => 'LineString', 'coordinates' => [ [574108.2557507273, 6222343.6199512165], @@ -120,7 +120,7 @@ public function testItReprojectsALineString(): void public function testItReprojectsAPolygonPreservingNesting(): void { - $geometry = $this->transformer->geometry(self::UTM32, [ + $geometry = $this->transformer->transformGeometry(self::UTM32, [ 'type' => 'Polygon', 'coordinates' => [ [ @@ -147,7 +147,7 @@ public function testItReprojectsAMultiPolygon(): void [574108.0, 6222343.0], ]; - $geometry = $this->transformer->geometry(self::UTM32, [ + $geometry = $this->transformer->transformGeometry(self::UTM32, [ 'type' => 'MultiPolygon', 'coordinates' => [[$ring], [$ring]], ]); @@ -160,14 +160,14 @@ public function testItRejectsAGeometryWithoutCoordinates(): void { $this->expectException(\InvalidArgumentException::class); - $this->transformer->geometry(self::UTM32, ['type' => 'Point']); + $this->transformer->transformGeometry(self::UTM32, ['type' => 'Point']); } public function testItRejectsAGeometryCollection(): void { $this->expectException(\InvalidArgumentException::class); - $this->transformer->geometry(self::UTM32, [ + $this->transformer->transformGeometry(self::UTM32, [ 'type' => 'GeometryCollection', 'geometries' => [], ]); @@ -177,6 +177,6 @@ public function testItRejectsAPositionWithASingleOrdinate(): void { $this->expectException(\InvalidArgumentException::class); - $this->transformer->geometry(self::UTM32, ['type' => 'Point', 'coordinates' => [574108.0]]); + $this->transformer->transformGeometry(self::UTM32, ['type' => 'Point', 'coordinates' => [574108.0]]); } } From 365325a037c8bb87612f5ad49dd223b174abd94c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 15:49:31 +0200 Subject: [PATCH 047/101] Coding standards --- docs/adr/007-source-manifest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/007-source-manifest.md b/docs/adr/007-source-manifest.md index 47fcea2..8bb90c3 100644 --- a/docs/adr/007-source-manifest.md +++ b/docs/adr/007-source-manifest.md @@ -10,7 +10,7 @@ ## Context -This application publishes data sets it does not own. This ADR serves +This application publishes data sets it does not own. This ADR serves to decide where the data sets and their specifications live. ### Drivers From 0ed759862c94c51100aa17f1a57940557278b389 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 09:47:31 +0200 Subject: [PATCH 048/101] Grouped the manifest classes under App\Source\Manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Catalog, Descriptor, Schema and Validator now sit together, which drops the repeated Source prefix. The cache warmer became Validator: it caches nothing, and exists so a malformed record fails the build. Schema no longer implements ConfigurationInterface — that is a bundle extension contract, not a description of a committed data file — so the catalogue hands the built tree to Processor::process() instead. --- .../Catalog.php} | 27 ++++++------- .../Descriptor.php} | 4 +- .../Schema.php} | 13 ++++--- src/Source/Manifest/Validator.php | 34 +++++++++++++++++ src/Source/MtmSpatialMaps/HandicapParking.php | 8 ++-- src/Source/SourceCatalogWarmer.php | 38 ------------------- .../CatalogTest.php} | 32 ++++++++-------- .../ValidatorTest.php} | 34 +++++++++-------- .../MtmSpatialMaps/HandicapParkingTest.php | 8 ++-- 9 files changed, 100 insertions(+), 98 deletions(-) rename src/Source/{SourceCatalog.php => Manifest/Catalog.php} (84%) rename src/Source/{SourceDescriptor.php => Manifest/Descriptor.php} (95%) rename src/Source/{SourceManifestConfiguration.php => Manifest/Schema.php} (92%) create mode 100644 src/Source/Manifest/Validator.php delete mode 100644 src/Source/SourceCatalogWarmer.php rename tests/Source/{SourceCatalogTest.php => Manifest/CatalogTest.php} (85%) rename tests/Source/{SourceCatalogWarmerTest.php => Manifest/ValidatorTest.php} (58%) diff --git a/src/Source/SourceCatalog.php b/src/Source/Manifest/Catalog.php similarity index 84% rename from src/Source/SourceCatalog.php rename to src/Source/Manifest/Catalog.php index 2b385df..41baa06 100644 --- a/src/Source/SourceCatalog.php +++ b/src/Source/Manifest/Catalog.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Source; +namespace App\Source\Manifest; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; @@ -13,18 +13,19 @@ /** * The manifest of data sets this application publishes. * - * The record's shape is a Symfony config tree rather than hand-written checks, - * and SourceCatalogWarmer reads the whole manifest during container warm-up, so - * a malformed entry fails the build instead of waiting for the one import that - * happens to select it. + * The record's shape is declared in Schema rather than checked by hand. Parsing + * is deferred to first use, and Validator reads every record when the + * application is built, so a malformed entry fails the build instead of waiting + * for the one import that happens to select it. * * @see config/sources.yaml - * @see SourceManifestConfiguration + * @see Schema + * @see Validator * @see docs/adr/007-source-manifest.md */ -final class SourceCatalog +final class Catalog { - /** @var array|null */ + /** @var array|null */ private ?array $descriptors = null; public function __construct( @@ -36,7 +37,7 @@ public function __construct( /** * @throws \RuntimeException when the manifest cannot be read, or carries no entry for the key */ - public function get(string $key): SourceDescriptor + public function get(string $key): Descriptor { $descriptors = $this->all(); @@ -48,7 +49,7 @@ public function get(string $key): SourceDescriptor } /** - * @return array keyed by source key + * @return array keyed by source key * * @throws \RuntimeException when the manifest cannot be read */ @@ -58,14 +59,14 @@ public function all(): array } /** - * @return array + * @return array */ private function load(): array { $descriptors = []; foreach ($this->validated() as $key => $entry) { - $descriptors[$key] = new SourceDescriptor( + $descriptors[$key] = new Descriptor( key: $key, title: $entry['title'], accessUrl: $entry['access_url'], @@ -111,7 +112,7 @@ private function validated(): array try { /** @var array}> $processed */ - $processed = new Processor()->processConfiguration(new SourceManifestConfiguration(), [$sources]); + $processed = new Processor()->process(Schema::tree(), [$sources]); } catch (InvalidConfigurationException $exception) { throw new \RuntimeException(\sprintf('Source manifest "%s" is invalid: %s', $this->manifest, $exception->getMessage()), previous: $exception); } diff --git a/src/Source/SourceDescriptor.php b/src/Source/Manifest/Descriptor.php similarity index 95% rename from src/Source/SourceDescriptor.php rename to src/Source/Manifest/Descriptor.php index 83b2b14..34a1e2b 100644 --- a/src/Source/SourceDescriptor.php +++ b/src/Source/Manifest/Descriptor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Source; +namespace App\Source\Manifest; /** * One entry from the source manifest: what a data set is, where it is read @@ -15,7 +15,7 @@ * @see config/sources.yaml * @see docs/adr/007-source-manifest.md */ -final readonly class SourceDescriptor +final readonly class Descriptor { /** * @param string $key identifier the import selects this data set by diff --git a/src/Source/SourceManifestConfiguration.php b/src/Source/Manifest/Schema.php similarity index 92% rename from src/Source/SourceManifestConfiguration.php rename to src/Source/Manifest/Schema.php index 05383d6..6d20aa3 100644 --- a/src/Source/SourceManifestConfiguration.php +++ b/src/Source/Manifest/Schema.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace App\Source; +namespace App\Source\Manifest; use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\NodeInterface; /** * The shape of the source manifest, as a Symfony config tree. @@ -15,12 +15,15 @@ * suffixed to that error as a hint, which is the moment a field's rationale is * needed. * + * Deliberately not a ConfigurationInterface: that is a bundle extension's + * contract, and this schema describes a committed data file instead. + * * @see config/sources.yaml * @see docs/adr/007-source-manifest.md */ -final readonly class SourceManifestConfiguration implements ConfigurationInterface +final readonly class Schema { - public function getConfigTreeBuilder(): TreeBuilder + public static function tree(): NodeInterface { $tree = new TreeBuilder('sources'); @@ -79,7 +82,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end(); - return $tree; + return $tree->buildTree(); } /** diff --git a/src/Source/Manifest/Validator.php b/src/Source/Manifest/Validator.php new file mode 100644 index 0000000..01d54dd --- /dev/null +++ b/src/Source/Manifest/Validator.php @@ -0,0 +1,34 @@ + nothing is written, so nothing is preloaded + */ + public function warmUp(string $cacheDir, ?string $buildDir = null): array + { + $this->catalog->all(); + + return []; + } +} diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 27bcdcd..b170ff0 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -7,8 +7,8 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\DataSourceReader; -use App\Source\SourceCatalog; -use App\Source\SourceDescriptor; +use App\Source\Manifest\Catalog; +use App\Source\Manifest\Descriptor; use App\Source\SourceInterface; /** @@ -24,7 +24,7 @@ public function __construct( private DataSourceReader $reader, private Wgs84Transformer $transformer, - private SourceCatalog $catalog, + private Catalog $catalog, ) { } @@ -50,7 +50,7 @@ public function entities(): iterable /** * @param array $feature GeoJSON Feature */ - private function toEntity(array $feature, SourceDescriptor $source): ?NgsiEntity + private function toEntity(array $feature, Descriptor $source): ?NgsiEntity { // A Feature keeps its attributes under `properties` and its geometry // beside them, so neither is at the feature's top level. diff --git a/src/Source/SourceCatalogWarmer.php b/src/Source/SourceCatalogWarmer.php deleted file mode 100644 index c262f5d..0000000 --- a/src/Source/SourceCatalogWarmer.php +++ /dev/null @@ -1,38 +0,0 @@ -catalog->all(); - - return []; - } -} diff --git a/tests/Source/SourceCatalogTest.php b/tests/Source/Manifest/CatalogTest.php similarity index 85% rename from tests/Source/SourceCatalogTest.php rename to tests/Source/Manifest/CatalogTest.php index 61a0f57..bfa83b5 100644 --- a/tests/Source/SourceCatalogTest.php +++ b/tests/Source/Manifest/CatalogTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace App\Tests\Source; +namespace App\Tests\Source\Manifest; -use App\Source\SourceCatalog; +use App\Source\Manifest\Catalog; use PHPUnit\Framework\TestCase; -class SourceCatalogTest extends TestCase +class CatalogTest extends TestCase { /** @var list */ private array $written = []; @@ -25,7 +25,7 @@ protected function tearDown(): void public function testTheShippedManifestIsUsable(): void { - $catalog = new SourceCatalog(\dirname(__DIR__, 2).'/config/sources.yaml'); + $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); $this->assertNotSame([], $catalog->all(), 'The manifest registers no data sets.'); } @@ -36,7 +36,7 @@ public function testTheShippedManifestIsUsable(): void */ public function testEveryShippedEntryCanBeImportedFrom(): void { - $catalog = new SourceCatalog(\dirname(__DIR__, 2).'/config/sources.yaml'); + $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); foreach ($catalog->all() as $key => $descriptor) { $this->assertSame($key, $descriptor->key); @@ -48,7 +48,7 @@ public function testEveryShippedEntryCanBeImportedFrom(): void public function testItNamesTheKnownEntriesWhenAskedForAnUnknownOne(): void { - $catalog = new SourceCatalog($this->manifest(<<<'YAML' + $catalog = new Catalog($this->manifest(<<<'YAML' sources: a-source: title: A source @@ -65,7 +65,7 @@ public function testItNamesTheKnownEntriesWhenAskedForAnUnknownOne(): void public function testItRejectsAManifestWithoutASourcesMapping(): void { - $catalog = new SourceCatalog($this->manifest("data_sets:\n a-source: {}\n")); + $catalog = new Catalog($this->manifest("data_sets:\n a-source: {}\n")); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('must contain a "sources" mapping'); @@ -75,7 +75,7 @@ public function testItRejectsAManifestWithoutASourcesMapping(): void public function testItRejectsAnEntryMissingAFieldTheImportNeeds(): void { - $catalog = new SourceCatalog($this->manifest(<<<'YAML' + $catalog = new Catalog($this->manifest(<<<'YAML' sources: a-source: title: A source @@ -91,7 +91,7 @@ public function testItRejectsAnEntryMissingAFieldTheImportNeeds(): void public function testItRejectsAnOmittedFieldWithoutAReason(): void { - $catalog = new SourceCatalog($this->manifest(<<<'YAML' + $catalog = new Catalog($this->manifest(<<<'YAML' sources: a-source: title: A source @@ -115,7 +115,7 @@ public function testItRejectsAnOmittedFieldWithoutAReason(): void */ public function testItKeepsADashedSourceKeyIntact(): void { - $catalog = new SourceCatalog($this->manifest(<<<'YAML' + $catalog = new Catalog($this->manifest(<<<'YAML' sources: handicap-parking: title: A source @@ -134,7 +134,7 @@ public function testItKeepsADashedSourceKeyIntact(): void */ public function testItRejectsAFieldTheManifestDoesNotDefine(): void { - $catalog = new SourceCatalog($this->manifest(<<<'YAML' + $catalog = new Catalog($this->manifest(<<<'YAML' sources: a-source: title: A source @@ -152,7 +152,7 @@ public function testItRejectsAFieldTheManifestDoesNotDefine(): void public function testItRejectsAnEntryThatIsNotAMapping(): void { - $catalog = new SourceCatalog($this->manifest("sources:\n a-source: just a string\n")); + $catalog = new Catalog($this->manifest("sources:\n a-source: just a string\n")); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Invalid type for path "sources.a-source"'); @@ -162,7 +162,7 @@ public function testItRejectsAnEntryThatIsNotAMapping(): void public function testItRejectsAManifestThatRegistersNothing(): void { - $catalog = new SourceCatalog($this->manifest("sources: {}\n")); + $catalog = new Catalog($this->manifest("sources: {}\n")); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('should have at least 1 element'); @@ -176,7 +176,7 @@ public function testItRejectsAManifestThatRegistersNothing(): void */ public function testItReadsABlankOptionalFieldAsUnknown(): void { - $catalog = new SourceCatalog($this->manifest(<<<'YAML' + $catalog = new Catalog($this->manifest(<<<'YAML' sources: a-source: title: A source @@ -197,7 +197,7 @@ public function testItReadsABlankOptionalFieldAsUnknown(): void public function testItReportsAManifestThatIsNotThere(): void { - $catalog = new SourceCatalog('/no/such/sources.yaml'); + $catalog = new Catalog('/no/such/sources.yaml'); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('does not exist'); @@ -207,7 +207,7 @@ public function testItReportsAManifestThatIsNotThere(): void public function testItReportsUnparsableYaml(): void { - $catalog = new SourceCatalog($this->manifest("sources:\n - [unbalanced\n")); + $catalog = new Catalog($this->manifest("sources:\n - [unbalanced\n")); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('is not valid YAML'); diff --git a/tests/Source/SourceCatalogWarmerTest.php b/tests/Source/Manifest/ValidatorTest.php similarity index 58% rename from tests/Source/SourceCatalogWarmerTest.php rename to tests/Source/Manifest/ValidatorTest.php index 02294d7..ada913d 100644 --- a/tests/Source/SourceCatalogWarmerTest.php +++ b/tests/Source/Manifest/ValidatorTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace App\Tests\Source; +namespace App\Tests\Source\Manifest; -use App\Source\SourceCatalog; -use App\Source\SourceCatalogWarmer; +use App\Source\Manifest\Catalog; +use App\Source\Manifest\Validator; use PHPUnit\Framework\TestCase; -class SourceCatalogWarmerTest extends TestCase +class ValidatorTest extends TestCase { /** @var list */ private array $written = []; @@ -25,28 +25,30 @@ protected function tearDown(): void } /** - * An optional warmer can be skipped, and a manifest that is only read when - * an import selects it is exactly what warming exists to avoid. + * A check that can be skipped is not a check. */ public function testItIsNotOptional(): void { - $this->assertFalse($this->warmer(\dirname(__DIR__, 2).'/config/sources.yaml')->isOptional()); + $this->assertFalse($this->validator(\dirname(__DIR__, 3).'/config/sources.yaml')->isOptional()); } - public function testItWarmsTheShippedManifestWithoutPreloadingAnything(): void + /** + * It validates rather than caches, so it leaves nothing behind to preload. + */ + public function testItAcceptsTheShippedManifestAndWritesNothing(): void { - $warmer = $this->warmer(\dirname(__DIR__, 2).'/config/sources.yaml'); + $validator = $this->validator(\dirname(__DIR__, 3).'/config/sources.yaml'); - $this->assertSame([], $warmer->warmUp(sys_get_temp_dir(), sys_get_temp_dir())); + $this->assertSame([], $validator->warmUp(sys_get_temp_dir(), sys_get_temp_dir())); } /** - * The reason for warming at all: an entry no import selects still fails the - * build rather than waiting to be discovered. + * The reason for checking at build time: an entry no import selects still + * fails the build rather than waiting to be discovered. */ public function testItFailsOnAnEntryNoImportWouldReach(): void { - $warmer = $this->warmer($this->manifest(<<<'YAML' + $validator = $this->validator($this->manifest(<<<'YAML' sources: a-source: title: A source @@ -62,12 +64,12 @@ public function testItFailsOnAnEntryNoImportWouldReach(): void $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('The child config "crs" under "sources.unreached-source" must be configured'); - $warmer->warmUp(sys_get_temp_dir(), sys_get_temp_dir()); + $validator->warmUp(sys_get_temp_dir(), sys_get_temp_dir()); } - private function warmer(string $manifest): SourceCatalogWarmer + private function validator(string $manifest): Validator { - return new SourceCatalogWarmer(new SourceCatalog($manifest)); + return new Validator(new Catalog($manifest)); } private function manifest(string $yaml): string diff --git a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php index 4a67b14..6517824 100644 --- a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php +++ b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php @@ -7,9 +7,9 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\DataSourceReader; +use App\Source\Manifest\Catalog; +use App\Source\Manifest\Descriptor; use App\Source\MtmSpatialMaps\HandicapParking; -use App\Source\SourceCatalog; -use App\Source\SourceDescriptor; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; @@ -22,7 +22,7 @@ class HandicapParkingTest extends TestCase { private const string KEY = 'mtm_spatialmaps-handicap-parking'; - private SourceDescriptor $source; + private Descriptor $source; private string $requestedUrl; @@ -31,7 +31,7 @@ class HandicapParkingTest extends TestCase protected function setUp(): void { - $catalog = new SourceCatalog(\dirname(__DIR__, 3).'/config/sources.yaml'); + $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); $this->source = $catalog->get(self::KEY); $client = new MockHttpClient(function (string $method, string $url): MockResponse { From 78c3538063736dbf0f98042d9224ea9c2cddc5c4 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 09:52:41 +0200 Subject: [PATCH 049/101] Cleaned up comments --- src/Source/Manifest/Catalog.php | 9 --------- src/Source/Manifest/Descriptor.php | 3 --- src/Source/Manifest/Schema.php | 8 -------- 3 files changed, 20 deletions(-) diff --git a/src/Source/Manifest/Catalog.php b/src/Source/Manifest/Catalog.php index 41baa06..722e589 100644 --- a/src/Source/Manifest/Catalog.php +++ b/src/Source/Manifest/Catalog.php @@ -13,11 +13,6 @@ /** * The manifest of data sets this application publishes. * - * The record's shape is declared in Schema rather than checked by hand. Parsing - * is deferred to first use, and Validator reads every record when the - * application is built, so a malformed entry fails the build instead of waiting - * for the one import that happens to select it. - * * @see config/sources.yaml * @see Schema * @see Validator @@ -101,10 +96,6 @@ private function validated(): array throw new \RuntimeException(\sprintf('Source manifest "%s" is not valid YAML: %s', $this->manifest, $exception->getMessage()), previous: $exception); } - // The tree is rooted at the entries themselves, so it never sees the - // key holding them. Without this the wrong shape yields an empty - // catalogue, which reads as "no data sets are registered" rather than - // as a broken file. $sources = \is_array($parsed) ? $parsed['sources'] ?? null : null; if (!\is_array($sources)) { throw new \RuntimeException(\sprintf('Source manifest "%s" must contain a "sources" mapping at the top level.', $this->manifest)); diff --git a/src/Source/Manifest/Descriptor.php b/src/Source/Manifest/Descriptor.php index 34a1e2b..13c66ad 100644 --- a/src/Source/Manifest/Descriptor.php +++ b/src/Source/Manifest/Descriptor.php @@ -5,9 +5,6 @@ namespace App\Source\Manifest; /** - * One entry from the source manifest: what a data set is, where it is read - * from, and on what terms. - * * Everything about a feed except its field mapping, which belongs to the * source class. The names follow DCAT-AP so that publishing the catalogue is a * rename rather than a second survey. diff --git a/src/Source/Manifest/Schema.php b/src/Source/Manifest/Schema.php index 6d20aa3..c4a977e 100644 --- a/src/Source/Manifest/Schema.php +++ b/src/Source/Manifest/Schema.php @@ -10,14 +10,6 @@ /** * The shape of the source manifest, as a Symfony config tree. * - * The tree is rooted at the entries rather than at the file, so a validation - * error names the path a maintainer sees in the manifest. Each `info()` is - * suffixed to that error as a hint, which is the moment a field's rationale is - * needed. - * - * Deliberately not a ConfigurationInterface: that is a bundle extension's - * contract, and this schema describes a committed data file instead. - * * @see config/sources.yaml * @see docs/adr/007-source-manifest.md */ From 05fa2386746f81b2887fe2910e05887c241f5d46 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 10:00:32 +0200 Subject: [PATCH 050/101] Minor correction in comment --- src/Source/Manifest/Descriptor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Source/Manifest/Descriptor.php b/src/Source/Manifest/Descriptor.php index 13c66ad..9f0e36b 100644 --- a/src/Source/Manifest/Descriptor.php +++ b/src/Source/Manifest/Descriptor.php @@ -6,8 +6,7 @@ /** * Everything about a feed except its field mapping, which belongs to the - * source class. The names follow DCAT-AP so that publishing the catalogue is a - * rename rather than a second survey. + * source class. The names follow DCAT-AP. * * @see config/sources.yaml * @see docs/adr/007-source-manifest.md From 603147dc81fa15be689805b3af22798271889808 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 11:23:18 +0200 Subject: [PATCH 051/101] Extracted the import logic into a DataSourceImporter service --- src/Import/DataSourceImporter.php | 132 ++++++++++++ src/Import/Exception/EmptySourceException.php | 20 ++ .../Exception/UnknownSourceException.php | 28 +++ .../Exception/UpsertFailedException.php | 19 ++ src/Import/ImportResult.php | 23 ++ tests/Import/DataSourceImporterTest.php | 201 ++++++++++++++++++ tests/Source/FakeSource.php | 61 ++++++ 7 files changed, 484 insertions(+) create mode 100644 src/Import/DataSourceImporter.php create mode 100644 src/Import/Exception/EmptySourceException.php create mode 100644 src/Import/Exception/UnknownSourceException.php create mode 100644 src/Import/Exception/UpsertFailedException.php create mode 100644 src/Import/ImportResult.php create mode 100644 tests/Import/DataSourceImporterTest.php create mode 100644 tests/Source/FakeSource.php diff --git a/src/Import/DataSourceImporter.php b/src/Import/DataSourceImporter.php new file mode 100644 index 0000000..6ae8f8c --- /dev/null +++ b/src/Import/DataSourceImporter.php @@ -0,0 +1,132 @@ + $sources + */ + public function __construct( + #[AutowireIterator('app.source')] + private iterable $sources, + private NgsiLdBroker $broker, + #[Autowire(env: 'ENTER_NGSI_CONTEXT_URLS')] + private string $contextUrls, + ) { + } + + /** + * @return list every registered source key, in registration order + */ + public function keys(): array + { + return array_keys($this->registry()); + } + + /** + * Converts a source to NGSI-LD without sending anything. + * + * @return non-empty-list> + * + * @throws UnknownSourceException when no source is registered under the key + * @throws EmptySourceException when the source yields no entities + */ + public function payload(string $key, ?int $limit = null): array + { + $registry = $this->registry(); + + if (!isset($registry[$key])) { + throw new UnknownSourceException($key, array_keys($registry)); + } + + // A limit below 1 is a mistyped option rather than a request to import + // nothing, and importing nothing is the one outcome this class refuses + // to report as a success. + $limit = null === $limit ? null : max(1, $limit); + $contexts = $this->contexts(); + + $payload = []; + foreach ($registry[$key]->entities() as $entity) { + $payload[] = $entity->toArray($contexts); + + // A source reads a whole feed lazily, so the limit stops the + // conversion instead of trimming its result. + if (null !== $limit && \count($payload) >= $limit) { + break; + } + } + + // A source that yields nothing is almost always misconfigured rather + // than genuinely empty, and it fails silently by construction: a + // record skipped for a missing field looks exactly like a feed with no + // records. Fail loudly so it cannot be mistaken for a successful run. + if ([] === $payload) { + throw new EmptySourceException($key); + } + + return $payload; + } + + /** + * @throws UnknownSourceException when no source is registered under the key + * @throws EmptySourceException when the source yields no entities + * @throws UpsertFailedException when the broker cannot be written to + */ + public function import(string $key, ?int $limit = null): ImportResult + { + $payload = $this->payload($key, $limit); + + try { + $status = $this->broker->upsert($payload); + } catch (\Throwable $exception) { + // A broker that is down or rejects the batch is an operational + // condition, not a bug in the conversion. Giving it a type of its + // own lets a caller report it plainly while the exceptions a + // broken feed raises — the ones worth a stack trace — pass through. + throw new UpsertFailedException($exception); + } + + return new ImportResult(\count($payload), $status, $this->broker->brokerUrl()); + } + + /** + * @return array keyed by source key + */ + private function registry(): array + { + $registry = []; + + foreach ($this->sources as $source) { + $registry[$source->key()] = $source; + } + + return $registry; + } + + /** + * @return list + */ + private function contexts(): array + { + return array_values(array_filter(array_map(trim(...), explode(',', $this->contextUrls)))); + } +} diff --git a/src/Import/Exception/EmptySourceException.php b/src/Import/Exception/EmptySourceException.php new file mode 100644 index 0000000..5a87617 --- /dev/null +++ b/src/Import/Exception/EmptySourceException.php @@ -0,0 +1,20 @@ + $known every key that is registered + */ + public function __construct( + public readonly string $key, + public readonly array $known, + ) { + parent::__construct(\sprintf( + 'Unknown source "%s". Available: %s.', + $key, + [] === $known ? 'none' : implode(', ', $known) + )); + } +} diff --git a/src/Import/Exception/UpsertFailedException.php b/src/Import/Exception/UpsertFailedException.php new file mode 100644 index 0000000..1e18c24 --- /dev/null +++ b/src/Import/Exception/UpsertFailedException.php @@ -0,0 +1,19 @@ +getMessage(), previous: $previous); + } +} diff --git a/src/Import/ImportResult.php b/src/Import/ImportResult.php new file mode 100644 index 0000000..f891f65 --- /dev/null +++ b/src/Import/ImportResult.php @@ -0,0 +1,23 @@ + $sources + */ + private function importer( + iterable $sources, + ?MockHttpClient $client = null, + string $contextUrls = self::CONTEXT_URLS, + ): DataSourceImporter { + return new DataSourceImporter( + $sources, + new NgsiLdBroker($client ?? new MockHttpClient(), self::BROKER_URL), + $contextUrls, + ); + } + + public function testItListsTheRegisteredSourceKeys(): void + { + $importer = $this->importer([FakeSource::withEntities('a-source'), FakeSource::withEntities('b-source')]); + + $this->assertSame(['a-source', 'b-source'], $importer->keys()); + } + + public function testItListsNothingWhenNoSourceIsRegistered(): void + { + $this->assertSame([], $this->importer([])->keys()); + } + + public function testItRejectsAnUnknownSourceAndNamesTheKnownOnes(): void + { + $importer = $this->importer([FakeSource::withEntities('some-source')]); + + $this->expectException(UnknownSourceException::class); + $this->expectExceptionMessage('Unknown source "nope". Available: some-source.'); + + $importer->payload('nope'); + } + + /** + * The important one: a source yielding nothing used to be reported as a + * completed import, which is indistinguishable from a working one. + */ + public function testItFailsWhenASourceProducesNothing(): void + { + $importer = $this->importer([new FakeSource('empty-source')]); + + $this->expectException(EmptySourceException::class); + $this->expectExceptionMessage('Source "empty-source" produced no entities.'); + + $importer->payload('empty-source'); + } + + /** + * The guard belongs to the conversion rather than to the dry run, so an + * import that intends to send is held to it too. + */ + public function testItSendsNothingWhenASourceProducesNothing(): void + { + $client = new MockHttpClient(); + + try { + $this->importer([new FakeSource('empty-source')], $client)->import('empty-source'); + $this->fail('An empty source was reported as a completed import.'); + } catch (EmptySourceException) { + $this->assertSame(0, $client->getRequestsCount(), 'An empty payload was sent to the broker.'); + } + } + + public function testEveryEntityCarriesTheConfiguredContexts(): void + { + $importer = $this->importer([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')]); + + $payload = $importer->payload('one-entity'); + + $this->assertSame( + ['https://example.com/domain.jsonld', 'https://example.com/core.jsonld'], + $payload[0]['@context'] + ); + } + + /** + * The contexts arrive as one comma-separated environment variable, so they + * are written by hand and carry whatever spacing that produces. + */ + public function testItIgnoresSpacingAndEmptyEntriesInTheConfiguredContexts(): void + { + $importer = $this->importer( + [FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], + contextUrls: ' https://example.com/domain.jsonld , ,', + ); + + $payload = $importer->payload('one-entity'); + + $this->assertSame(['https://example.com/domain.jsonld'], $payload[0]['@context']); + } + + public function testALimitCapsThePayload(): void + { + $importer = $this->importer([FakeSource::withEntities('many', 'urn:1', 'urn:2', 'urn:3', 'urn:4', 'urn:5')]); + + $this->assertCount(2, $importer->payload('many', 2)); + } + + /** + * A real source reads a whole feed, so a limit that converts everything + * and then trims would do all the work it was given to avoid. + */ + public function testALimitStopsPullingFromTheSource(): void + { + $source = FakeSource::withEntities('many', 'urn:1', 'urn:2', 'urn:3', 'urn:4', 'urn:5'); + + $this->importer([$source])->payload('many', 2); + + $this->assertSame(2, $source->produced()); + } + + /** + * --limit 0 is a mistyped option. Honouring it would produce an empty + * payload, which is the one outcome an import refuses to call a success. + */ + public function testALimitBelowOneStillImportsOneEntity(): void + { + $importer = $this->importer([FakeSource::withEntities('many', 'urn:1', 'urn:2')]); + + $this->assertCount(1, $importer->payload('many', 0)); + } + + public function testBuildingThePayloadSendsNothing(): void + { + $client = new MockHttpClient(); + + $this->importer([FakeSource::withEntities('one-entity', 'urn:1')], $client)->payload('one-entity'); + + $this->assertSame(0, $client->getRequestsCount()); + } + + public function testItUpsertsThePayloadAndReportsWhatTheBrokerDid(): void + { + $client = new MockHttpClient(new MockResponse('', ['http_code' => 204])); + + $result = $this->importer([FakeSource::withEntities('many', 'urn:1', 'urn:2')], $client)->import('many'); + + $this->assertSame(2, $result->count); + $this->assertSame(204, $result->status); + $this->assertSame(self::BROKER_URL, $result->brokerUrl); + $this->assertSame(1, $client->getRequestsCount(), 'The entities were not sent as one batch.'); + } + + public function testItReportsWhatTheBrokerSaidWhenTheUpsertIsRejected(): void + { + $client = new MockHttpClient(new MockResponse('{"title":"Bad Request"}', ['http_code' => 400])); + + try { + $this->importer([FakeSource::withEntities('one-entity', 'urn:1')], $client)->import('one-entity'); + $this->fail('A rejected upsert was reported as a completed import.'); + } catch (UpsertFailedException $exception) { + $this->assertStringContainsString('HTTP 400', $exception->getMessage()); + $this->assertStringContainsString('Bad Request', $exception->getMessage()); + $this->assertNotNull($exception->getPrevious(), 'The broker\'s own exception was discarded.'); + } + } + + /** + * A broker that cannot be reached fails in the HTTP client rather than in + * the broker's status check, and the two are the same thing to a caller. + */ + public function testItFailsTheSameWayWhenTheBrokerCannotBeReached(): void + { + $client = new MockHttpClient(static function (): never { + throw new TransportException('Connection refused'); + }); + + $this->expectException(UpsertFailedException::class); + $this->expectExceptionMessage('Connection refused'); + + $this->importer([FakeSource::withEntities('one-entity', 'urn:1')], $client)->import('one-entity'); + } +} diff --git a/tests/Source/FakeSource.php b/tests/Source/FakeSource.php new file mode 100644 index 0000000..052ab33 --- /dev/null +++ b/tests/Source/FakeSource.php @@ -0,0 +1,61 @@ + $entities + */ + public function __construct( + private readonly string $key, + private readonly array $entities = [], + ) { + } + + /** + * @param string ...$ids entity ids, one entity each + */ + public static function withEntities(string $key, string ...$ids): self + { + return new self($key, array_map( + static fn (string $id): NgsiEntity => new NgsiEntity($id, 'Example'), + array_values($ids) + )); + } + + public function key(): string + { + return $this->key; + } + + public function entities(): iterable + { + foreach ($this->entities as $entity) { + ++$this->produced; + + yield $entity; + } + } + + /** + * How many entities were actually pulled from this source. + */ + public function produced(): int + { + return $this->produced; + } +} From 07aec4f9ddb4db0c3752e9249059bda65857dae0 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 11:24:05 +0200 Subject: [PATCH 052/101] Reduced ImportCommand to console plumbing --- src/Command/ImportCommand.php | 97 ++++++++------------ src/Source/SourceInterface.php | 4 +- tests/Command/ImportCommandTest.php | 131 +++++++++++++--------------- 3 files changed, 97 insertions(+), 135 deletions(-) diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php index b0c6a46..e1c4fa9 100644 --- a/src/Command/ImportCommand.php +++ b/src/Command/ImportCommand.php @@ -4,8 +4,10 @@ namespace App\Command; -use App\Broker\NgsiLdBroker; -use App\Source\SourceInterface; +use App\Import\DataSourceImporter; +use App\Import\Exception\EmptySourceException; +use App\Import\Exception\UnknownSourceException; +use App\Import\Exception\UpsertFailedException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -13,9 +15,11 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; +/** + * The console front for DataSourceImporter: selects a source, and turns what + * the import raises into an exit code and something readable. + */ #[AsCommand( name: 'app:import', description: 'Convert a data source to NGSI-LD and upsert it into the context broker.', @@ -24,15 +28,8 @@ final class ImportCommand extends Command { private const JSON_FLAGS = \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; - /** - * @param iterable $sources - */ public function __construct( - #[AutowireIterator('app.source')] - private readonly iterable $sources, - private readonly NgsiLdBroker $broker, - #[Autowire(env: 'ENTER_NGSI_CONTEXT_URLS')] - private readonly string $contextUrls, + private readonly DataSourceImporter $importer, ) { parent::__construct(); } @@ -49,12 +46,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $sources = []; - foreach ($this->sources as $source) { - $sources[$source->key()] = $source; - } + $keys = $this->importer->keys(); - if ([] === $sources) { + if ([] === $keys) { $io->error('No data sources are registered.'); $io->listing([ 'A source must implement App\Source\SourceInterface.', @@ -64,50 +58,46 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $key = $input->getArgument('source'); + $argument = $input->getArgument('source'); - if (null === $key) { + if (null === $argument) { // Options only make sense together with a source. Listing the // sources and exiting successfully would look like an import ran. if ($input->getOption('dry-run') || null !== $input->getOption('limit')) { $io->error(\sprintf( 'No source given. Available: %s.', - implode(', ', array_keys($sources)) + implode(', ', $keys) )); return Command::INVALID; } $io->section('Available sources'); - $io->listing(array_keys($sources)); + $io->listing($keys); return Command::SUCCESS; } - if (!isset($sources[$key])) { - $io->error(\sprintf('Unknown source "%s". Available: %s.', $key, implode(', ', array_keys($sources)))); + $key = (string) $argument; + $limit = null !== $input->getOption('limit') ? (int) $input->getOption('limit') : null; - return Command::INVALID; - } - - $limit = null !== $input->getOption('limit') ? max(1, (int) $input->getOption('limit')) : null; - $contexts = $this->contexts(); + try { + if ($input->getOption('dry-run')) { + $payload = $this->importer->payload($key, $limit); - $payload = []; - foreach ($sources[$key]->entities() as $entity) { - $payload[] = $entity->toArray($contexts); + $output->writeln(json_encode($payload, self::JSON_FLAGS)); + $io->note(\sprintf('Dry run: %d entities were not sent.', \count($payload))); - if (null !== $limit && \count($payload) >= $limit) { - break; + return Command::SUCCESS; } - } - // A source that yields nothing is almost always misconfigured rather - // than genuinely empty, and it fails silently by construction: a - // record skipped for a missing field looks exactly like a feed with no - // records. Fail loudly so it cannot be mistaken for a successful run. - if ([] === $payload) { - $io->error(\sprintf('Source "%s" produced no entities.', $key)); + $result = $this->importer->import($key, $limit); + } catch (UnknownSourceException $exception) { + $io->error($exception->getMessage()); + + return Command::INVALID; + } catch (EmptySourceException $exception) { + $io->error($exception->getMessage()); $io->text( 'The source ran to completion without raising an exception, so every record was ' .'discarded by the source\'s own guards rather than failing. Verbosity flags will ' @@ -120,18 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]); return Command::FAILURE; - } - - if ($input->getOption('dry-run')) { - $output->writeln(json_encode($payload, self::JSON_FLAGS)); - $io->note(\sprintf('Dry run: %d entities were not sent.', \count($payload))); - - return Command::SUCCESS; - } - - try { - $status = $this->broker->upsert($payload); - } catch (\Throwable $exception) { + } catch (UpsertFailedException $exception) { $io->error($exception->getMessage()); return Command::FAILURE; @@ -139,19 +118,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->success(\sprintf( 'Upserted %d entities into %s (HTTP %d).', - \count($payload), - $this->broker->brokerUrl(), - $status + $result->count, + $result->brokerUrl, + $result->status )); return Command::SUCCESS; } - - /** - * @return list - */ - private function contexts(): array - { - return array_values(array_filter(array_map(trim(...), explode(',', $this->contextUrls)))); - } } diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php index b372d28..eaea6a7 100644 --- a/src/Source/SourceInterface.php +++ b/src/Source/SourceInterface.php @@ -12,8 +12,8 @@ * * Implementations own everything specific to their feed: where it comes from, * its field names, its quirks, and which Smart Data Model it maps onto. The - * broker and the import command stay unaware of all of it, so adding the next - * ENTER data set means adding one class and nothing else. + * broker and the import stay unaware of all of it, so adding the next ENTER + * data set means adding one class and nothing else. */ #[AutoconfigureTag('app.source')] interface SourceInterface diff --git a/tests/Command/ImportCommandTest.php b/tests/Command/ImportCommandTest.php index a1d68ad..c337ed1 100644 --- a/tests/Command/ImportCommandTest.php +++ b/tests/Command/ImportCommandTest.php @@ -6,72 +6,42 @@ use App\Broker\NgsiLdBroker; use App\Command\ImportCommand; -use App\Ngsi\NgsiEntity; +use App\Import\DataSourceImporter; use App\Source\SourceInterface; +use App\Tests\Source\FakeSource; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +/** + * The console surface only: the affordances around an import, and which exit + * code each outcome maps to. What an import decides belongs to + * DataSourceImporter and is covered by DataSourceImporterTest. + */ class ImportCommandTest extends TestCase { /** * @param iterable $sources */ - private function tester(iterable $sources): CommandTester + private function tester(iterable $sources, ?MockHttpClient $client = null): CommandTester { - return new CommandTester(new ImportCommand( + return new CommandTester(new ImportCommand(new DataSourceImporter( $sources, - new NgsiLdBroker(new MockHttpClient(), 'http://broker.invalid'), + new NgsiLdBroker($client ?? new MockHttpClient(), 'http://broker.invalid'), 'https://example.com/context.jsonld', - )); + ))); } - private function source(string $key, NgsiEntity ...$entities): SourceInterface + public function testItListsTheSourcesWhenCalledBare(): void { - return new readonly class($key, $entities) implements SourceInterface { - /** @param list $entities */ - public function __construct( - private string $key, - private array $entities, - ) { - } - - public function key(): string - { - return $this->key; - } - - public function entities(): iterable - { - yield from $this->entities; - } - }; - } - - /** - * The important one: a source yielding nothing used to exit successfully - * with a warning, which is indistinguishable from a working import. - */ - public function testItFailsWhenASourceProducesNothing(): void - { - $tester = $this->tester([$this->source('empty-source')]); - - $status = $tester->execute(['source' => 'empty-source', '--dry-run' => true]); - - $this->assertSame(Command::FAILURE, $status); - $this->assertStringContainsString('produced no entities', $tester->getDisplay()); - } - - public function testItSuggestsCausesWhenASourceProducesNothing(): void - { - $tester = $this->tester([$this->source('empty-source')]); - $tester->execute(['source' => 'empty-source', '--dry-run' => true]); + $tester = $this->tester([FakeSource::withEntities('some-source')]); - $display = $tester->getDisplay(); + $status = $tester->execute([]); - $this->assertStringContainsString('path or URL', $display); - $this->assertStringContainsString('envelope, nesting, field names', $display); + $this->assertSame(Command::SUCCESS, $status); + $this->assertStringContainsString('some-source', $tester->getDisplay()); } public function testItFailsWhenNoSourcesAreRegistered(): void @@ -90,7 +60,7 @@ public function testItFailsWhenNoSourcesAreRegistered(): void */ public function testItRejectsOptionsWithoutASource(): void { - $tester = $this->tester([$this->source('some-source')]); + $tester = $this->tester([FakeSource::withEntities('some-source')]); $status = $tester->execute(['--dry-run' => true]); @@ -98,32 +68,37 @@ public function testItRejectsOptionsWithoutASource(): void $this->assertStringContainsString('No source given', $tester->getDisplay()); } - public function testItStillListsSourcesWhenCalledBare(): void + public function testAnUnknownSourceIsTheCallersMistake(): void { - $tester = $this->tester([$this->source('some-source')]); + $tester = $this->tester([FakeSource::withEntities('some-source')]); - $status = $tester->execute([]); + $status = $tester->execute(['source' => 'nope']); - $this->assertSame(Command::SUCCESS, $status); - $this->assertStringContainsString('some-source', $tester->getDisplay()); + $this->assertSame(Command::INVALID, $status); + $this->assertStringContainsString('Unknown source "nope"', $tester->getDisplay()); } - public function testItRejectsAnUnknownSource(): void + /** + * The failure carries no exception to show, so the command has to supply + * the places worth looking itself. + */ + public function testAnEmptySourceFailsAndSuggestsCauses(): void { - $tester = $this->tester([$this->source('some-source')]); + $tester = $this->tester([new FakeSource('empty-source')]); - $status = $tester->execute(['source' => 'nope']); + $status = $tester->execute(['source' => 'empty-source']); + $display = $tester->getDisplay(); - $this->assertSame(Command::INVALID, $status); - $this->assertStringContainsString('Unknown source "nope"', $tester->getDisplay()); + $this->assertSame(Command::FAILURE, $status); + $this->assertStringContainsString('produced no entities', $display); + $this->assertStringContainsString('path or URL', $display); + $this->assertStringContainsString('envelope, nesting, field names', $display); } public function testDryRunPrintsThePayloadAndSendsNothing(): void { - $entity = new NgsiEntity('urn:ngsi-ld:Example:1', 'Example') - ->setProperty('name', 'Example'); - - $tester = $this->tester([$this->source('one-entity', $entity)]); + $client = new MockHttpClient(); + $tester = $this->tester([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], $client); $status = $tester->execute(['source' => 'one-entity', '--dry-run' => true]); $display = $tester->getDisplay(); @@ -131,18 +106,34 @@ public function testDryRunPrintsThePayloadAndSendsNothing(): void $this->assertSame(Command::SUCCESS, $status); $this->assertStringContainsString('"urn:ngsi-ld:Example:1"', $display); $this->assertStringContainsString('1 entities were not sent', $display); + $this->assertSame(0, $client->getRequestsCount()); } - public function testLimitCapsThePayload(): void + public function testItReportsWhatWasUpserted(): void { - $entities = []; - foreach (range(1, 5) as $i) { - $entities[] = new NgsiEntity(\sprintf('urn:ngsi-ld:Example:%d', $i), 'Example'); - } + $client = new MockHttpClient(new MockResponse('', ['http_code' => 204])); + $tester = $this->tester([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], $client); - $tester = $this->tester([$this->source('many', ...$entities)]); - $tester->execute(['source' => 'many', '--dry-run' => true, '--limit' => 2]); + $status = $tester->execute(['source' => 'one-entity']); + $display = $tester->getDisplay(); + + $this->assertSame(Command::SUCCESS, $status); + $this->assertStringContainsString('Upserted 1 entities', $display); + $this->assertStringContainsString('HTTP 204', $display); + } + + /** + * The broker being down is an operational condition rather than a bug, so + * it is reported as a message instead of an uncaught exception. + */ + public function testABrokerFailureIsReportedAsAnError(): void + { + $client = new MockHttpClient(new MockResponse('', ['http_code' => 500])); + $tester = $this->tester([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], $client); - $this->assertStringContainsString('2 entities were not sent', $tester->getDisplay()); + $status = $tester->execute(['source' => 'one-entity']); + + $this->assertSame(Command::FAILURE, $status); + $this->assertStringContainsString('HTTP 500', $tester->getDisplay()); } } From 64aa10f06153385d43d39f205c3b7bbb811616c8 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 12:57:54 +0200 Subject: [PATCH 053/101] Move context from comma seperated list in .env to each dataset via config/sources.yaml --- .env | 2 +- config/sources.yaml | 1 + src/Import/DataSourceImporter.php | 47 +++++++-------- src/Source/Manifest/Catalog.php | 5 +- src/Source/Manifest/Descriptor.php | 1 + src/Source/Manifest/Schema.php | 5 ++ tests/Command/ImportCommandTest.php | 12 +++- tests/Import/DataSourceImporterTest.php | 72 ++++++++++++++++++++--- tests/Source/Manifest/CatalogTest.php | 7 +++ tests/Source/Manifest/ValidatorTest.php | 2 + tests/Source/Manifest/WritesManifests.php | 61 +++++++++++++++++++ 11 files changed, 181 insertions(+), 34 deletions(-) create mode 100644 tests/Source/Manifest/WritesManifests.php diff --git a/.env b/.env index a9d7b27..b68e4c9 100644 --- a/.env +++ b/.env @@ -36,7 +36,7 @@ APP_BROKER_BASE_URI=http://scorpio.local:9090/ # JSON-LD contexts attached to every entity, outermost last so the ETSI core # context resolves the NGSI-LD terms and the domain context resolves the # Smart Data Models ones. -ENTER_NGSI_CONTEXT_URLS='https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld,https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld' +ENTER_NGSI_CONTEXT_URLS='https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld' # A single domain context, for the Link header that read requests need. Only # used by `task broker:entities`, and it must be the context defining the type diff --git a/config/sources.yaml b/config/sources.yaml index b011401..bebb760 100644 --- a/config/sources.yaml +++ b/config/sources.yaml @@ -17,6 +17,7 @@ sources: media_type: application/geo+json crs: 'EPSG:25832' model: OnStreetParking + context_url: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld' update_frequency: continuous # The portal states no licence for this data set. DCAT-AP requires diff --git a/src/Import/DataSourceImporter.php b/src/Import/DataSourceImporter.php index 6ae8f8c..3030870 100644 --- a/src/Import/DataSourceImporter.php +++ b/src/Import/DataSourceImporter.php @@ -8,17 +8,13 @@ use App\Import\Exception\EmptySourceException; use App\Import\Exception\UnknownSourceException; use App\Import\Exception\UpsertFailedException; +use App\Source\Manifest\Catalog; use App\Source\SourceInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; /** * Converts a registered source to NGSI-LD and upserts it into the broker. - * - * Owns what an import decides: which sources exist, which contexts their - * entities carry, how many to take, and what counts as a failed run. That - * leaves the command around it with argument parsing and exit codes, and lets - * the decisions be exercised without a console. */ final readonly class DataSourceImporter { @@ -28,6 +24,7 @@ public function __construct( #[AutowireIterator('app.source')] private iterable $sources, + private Catalog $catalog, private NgsiLdBroker $broker, #[Autowire(env: 'ENTER_NGSI_CONTEXT_URLS')] private string $contextUrls, @@ -43,7 +40,7 @@ public function keys(): array } /** - * Converts a source to NGSI-LD without sending anything. + * Converts a source to NGSI-LD. * * @return non-empty-list> * @@ -52,33 +49,30 @@ public function keys(): array */ public function payload(string $key, ?int $limit = null): array { + // Collect all dataset keys. $registry = $this->registry(); + // Check if requested dataset key exists. if (!isset($registry[$key])) { throw new UnknownSourceException($key, array_keys($registry)); } - // A limit below 1 is a mistyped option rather than a request to import - // nothing, and importing nothing is the one outcome this class refuses - // to report as a success. + // Define minimum limit, in case of limit defined as less than 1. $limit = null === $limit ? null : max(1, $limit); - $contexts = $this->contexts(); + + // Load context for given dataset. + $contexts = $this->contexts($key); $payload = []; foreach ($registry[$key]->entities() as $entity) { $payload[] = $entity->toArray($contexts); - // A source reads a whole feed lazily, so the limit stops the - // conversion instead of trimming its result. + // Break upon limit. if (null !== $limit && \count($payload) >= $limit) { break; } } - // A source that yields nothing is almost always misconfigured rather - // than genuinely empty, and it fails silently by construction: a - // record skipped for a missing field looks exactly like a feed with no - // records. Fail loudly so it cannot be mistaken for a successful run. if ([] === $payload) { throw new EmptySourceException($key); } @@ -93,22 +87,24 @@ public function payload(string $key, ?int $limit = null): array */ public function import(string $key, ?int $limit = null): ImportResult { + // Get payload from dataset. $payload = $this->payload($key, $limit); + // Try to upsert broker with payload. try { $status = $this->broker->upsert($payload); } catch (\Throwable $exception) { - // A broker that is down or rejects the batch is an operational - // condition, not a bug in the conversion. Giving it a type of its - // own lets a caller report it plainly while the exceptions a - // broken feed raises — the ones worth a stack trace — pass through. throw new UpsertFailedException($exception); } + // Return result. return new ImportResult(\count($payload), $status, $this->broker->brokerUrl()); } /** + * Get list of registered datasets. + * + * @see config/sources.yaml * @return array keyed by source key */ private function registry(): array @@ -123,10 +119,15 @@ private function registry(): array } /** - * @return list + * Return an array of contexts. Each dataset holds its own context + * @see config/sources.yaml + * @return array */ - private function contexts(): array + private function contexts(string $key): array { - return array_values(array_filter(array_map(trim(...), explode(',', $this->contextUrls)))); + return [ + $this->catalog->get($key)->contextUrl, + ...array_values(array_filter(array_map(trim(...), explode(',', $this->contextUrls)))), + ]; } } diff --git a/src/Source/Manifest/Catalog.php b/src/Source/Manifest/Catalog.php index 722e589..71ad47f 100644 --- a/src/Source/Manifest/Catalog.php +++ b/src/Source/Manifest/Catalog.php @@ -67,6 +67,7 @@ private function load(): array accessUrl: $entry['access_url'], crs: $entry['crs'], model: $entry['model'], + contextUrl: $entry['context_url'], description: $entry['description'], publisher: $entry['publisher'], contact: $entry['contact'], @@ -82,7 +83,7 @@ private function load(): array } /** - * @return array}> + * @return array}> */ private function validated(): array { @@ -102,7 +103,7 @@ private function validated(): array } try { - /** @var array}> $processed */ + /** @var array}> $processed */ $processed = new Processor()->process(Schema::tree(), [$sources]); } catch (InvalidConfigurationException $exception) { throw new \RuntimeException(\sprintf('Source manifest "%s" is invalid: %s', $this->manifest, $exception->getMessage()), previous: $exception); diff --git a/src/Source/Manifest/Descriptor.php b/src/Source/Manifest/Descriptor.php index 9f0e36b..46d0631 100644 --- a/src/Source/Manifest/Descriptor.php +++ b/src/Source/Manifest/Descriptor.php @@ -26,6 +26,7 @@ public function __construct( public string $accessUrl, public string $crs, public string $model, + public string $contextUrl, public ?string $description = null, public ?string $publisher = null, public ?string $contact = null, diff --git a/src/Source/Manifest/Schema.php b/src/Source/Manifest/Schema.php index c4a977e..bbaf41c 100644 --- a/src/Source/Manifest/Schema.php +++ b/src/Source/Manifest/Schema.php @@ -50,6 +50,11 @@ public static function tree(): NodeInterface ->cannotBeEmpty() ->info('Smart Data Model the data set is published as.') ->end() + ->scalarNode('context_url') + ->isRequired() + ->cannotBeEmpty() + ->info('JSON-LD context defining the model\'s terms. Without the right one a consumer reads the entity\'s attributes as undefined strings.') + ->end() ->scalarNode('description')->defaultNull()->end() ->scalarNode('publisher')->defaultNull()->end() ->scalarNode('contact')->defaultNull()->end() diff --git a/tests/Command/ImportCommandTest.php b/tests/Command/ImportCommandTest.php index c337ed1..6d4c7ca 100644 --- a/tests/Command/ImportCommandTest.php +++ b/tests/Command/ImportCommandTest.php @@ -7,8 +7,10 @@ use App\Broker\NgsiLdBroker; use App\Command\ImportCommand; use App\Import\DataSourceImporter; +use App\Source\Manifest\Catalog; use App\Source\SourceInterface; use App\Tests\Source\FakeSource; +use App\Tests\Source\Manifest\WritesManifests; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; @@ -22,15 +24,23 @@ */ class ImportCommandTest extends TestCase { + use WritesManifests; + /** * @param iterable $sources */ private function tester(iterable $sources, ?MockHttpClient $client = null): CommandTester { + $keys = []; + foreach ($sources as $source) { + $keys[] = $source->key(); + } + return new CommandTester(new ImportCommand(new DataSourceImporter( $sources, + new Catalog($this->manifestFor($keys)), new NgsiLdBroker($client ?? new MockHttpClient(), 'http://broker.invalid'), - 'https://example.com/context.jsonld', + 'https://example.com/core.jsonld', ))); } diff --git a/tests/Import/DataSourceImporterTest.php b/tests/Import/DataSourceImporterTest.php index a1c666a..3e838a7 100644 --- a/tests/Import/DataSourceImporterTest.php +++ b/tests/Import/DataSourceImporterTest.php @@ -9,8 +9,10 @@ use App\Import\Exception\EmptySourceException; use App\Import\Exception\UnknownSourceException; use App\Import\Exception\UpsertFailedException; +use App\Source\Manifest\Catalog; use App\Source\SourceInterface; use App\Tests\Source\FakeSource; +use App\Tests\Source\Manifest\WritesManifests; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\MockHttpClient; @@ -18,20 +20,27 @@ class DataSourceImporterTest extends TestCase { + use WritesManifests; private const string BROKER_URL = 'http://broker.invalid'; - private const string CONTEXT_URLS = 'https://example.com/domain.jsonld,https://example.com/core.jsonld'; + private const string CORE_CONTEXT = 'https://example.com/core.jsonld'; /** - * @param iterable $sources + * @param iterable $sources each is registered in the manifest under its own key */ private function importer( iterable $sources, ?MockHttpClient $client = null, - string $contextUrls = self::CONTEXT_URLS, + string $contextUrls = self::CORE_CONTEXT, ): DataSourceImporter { + $keys = []; + foreach ($sources as $source) { + $keys[] = $source->key(); + } + return new DataSourceImporter( $sources, + new Catalog($this->manifestFor($keys)), new NgsiLdBroker($client ?? new MockHttpClient(), self::BROKER_URL), $contextUrls, ); @@ -89,18 +98,64 @@ public function testItSendsNothingWhenASourceProducesNothing(): void } } - public function testEveryEntityCarriesTheConfiguredContexts(): void + /** + * Later entries win term conflicts, so the data set's own context comes + * first and the core context last, where it stays authoritative over the + * NGSI-LD terms a domain context may also define. + */ + public function testEveryEntityCarriesItsDataSetContextThenTheCoreContext(): void { $importer = $this->importer([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')]); $payload = $importer->payload('one-entity'); $this->assertSame( - ['https://example.com/domain.jsonld', 'https://example.com/core.jsonld'], + [self::dataSetContext('one-entity'), self::CORE_CONTEXT], $payload[0]['@context'] ); } + /** + * Two data sets published under different models must not advertise each + * other's vocabulary, which a single app-wide list cannot avoid. + */ + public function testEachDataSetCarriesOnlyItsOwnContext(): void + { + $importer = $this->importer([ + FakeSource::withEntities('first-source', 'urn:ngsi-ld:Example:1'), + FakeSource::withEntities('second-source', 'urn:ngsi-ld:Example:2'), + ]); + + $this->assertSame( + [self::dataSetContext('first-source'), self::CORE_CONTEXT], + $importer->payload('first-source')[0]['@context'] + ); + $this->assertSame( + [self::dataSetContext('second-source'), self::CORE_CONTEXT], + $importer->payload('second-source')[0]['@context'] + ); + } + + /** + * A source registered in the container but not in the manifest has no + * context to publish under, so it fails rather than emitting entities a + * consumer cannot resolve. + */ + public function testItFailsWhenASourceHasNoManifestEntry(): void + { + $importer = new DataSourceImporter( + [FakeSource::withEntities('unregistered', 'urn:ngsi-ld:Example:1')], + new Catalog($this->manifestFor(['other-source'])), + new NgsiLdBroker(new MockHttpClient(), self::BROKER_URL), + self::CORE_CONTEXT, + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('No entry for source "unregistered"'); + + $importer->payload('unregistered'); + } + /** * The contexts arrive as one comma-separated environment variable, so they * are written by hand and carry whatever spacing that produces. @@ -109,12 +164,15 @@ public function testItIgnoresSpacingAndEmptyEntriesInTheConfiguredContexts(): vo { $importer = $this->importer( [FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], - contextUrls: ' https://example.com/domain.jsonld , ,', + contextUrls: ' '.self::CORE_CONTEXT.' , ,', ); $payload = $importer->payload('one-entity'); - $this->assertSame(['https://example.com/domain.jsonld'], $payload[0]['@context']); + $this->assertSame( + [self::dataSetContext('one-entity'), self::CORE_CONTEXT], + $payload[0]['@context'] + ); } public function testALimitCapsThePayload(): void diff --git a/tests/Source/Manifest/CatalogTest.php b/tests/Source/Manifest/CatalogTest.php index bfa83b5..2707d49 100644 --- a/tests/Source/Manifest/CatalogTest.php +++ b/tests/Source/Manifest/CatalogTest.php @@ -43,6 +43,7 @@ public function testEveryShippedEntryCanBeImportedFrom(): void $this->assertMatchesRegularExpression('#^https?://#', $descriptor->accessUrl, $key); $this->assertMatchesRegularExpression('/^EPSG:\d+$/', $descriptor->crs, $key); $this->assertNotSame('', $descriptor->model, $key); + $this->assertMatchesRegularExpression('#^https?://#', $descriptor->contextUrl, $key); } } @@ -55,6 +56,7 @@ public function testItNamesTheKnownEntriesWhenAskedForAnUnknownOne(): void access_url: https://example.com/feed.json crs: EPSG:25832 model: Example + context_url: https://example.com/context.jsonld YAML)); $this->expectException(\RuntimeException::class); @@ -81,6 +83,7 @@ public function testItRejectsAnEntryMissingAFieldTheImportNeeds(): void title: A source access_url: https://example.com/feed.json model: Example + context_url: https://example.com/context.jsonld YAML)); $this->expectException(\RuntimeException::class); @@ -98,6 +101,7 @@ public function testItRejectsAnOmittedFieldWithoutAReason(): void access_url: https://example.com/feed.json crs: EPSG:25832 model: Example + context_url: https://example.com/context.jsonld omitted_fields: some_field: ~ YAML)); @@ -122,6 +126,7 @@ public function testItKeepsADashedSourceKeyIntact(): void access_url: https://example.com/feed.json crs: EPSG:25832 model: Example + context_url: https://example.com/context.jsonld YAML)); $this->assertSame(['handicap-parking'], array_keys($catalog->all())); @@ -141,6 +146,7 @@ public function testItRejectsAFieldTheManifestDoesNotDefine(): void access_url: https://example.com/feed.json crs: EPSG:25832 model: Example + context_url: https://example.com/context.jsonld license: CC-BY-4.0 YAML)); @@ -183,6 +189,7 @@ public function testItReadsABlankOptionalFieldAsUnknown(): void access_url: https://example.com/feed.json crs: EPSG:25832 model: Example + context_url: https://example.com/context.jsonld publisher: ' Aarhus Kommune ' contact: '' licence: ~ diff --git a/tests/Source/Manifest/ValidatorTest.php b/tests/Source/Manifest/ValidatorTest.php index ada913d..53230bb 100644 --- a/tests/Source/Manifest/ValidatorTest.php +++ b/tests/Source/Manifest/ValidatorTest.php @@ -55,10 +55,12 @@ public function testItFailsOnAnEntryNoImportWouldReach(): void access_url: https://example.com/feed.json crs: EPSG:25832 model: Example + context_url: https://example.com/context.jsonld unreached-source: title: Another source access_url: https://example.com/other.json model: Example + context_url: https://example.com/context.jsonld YAML)); $this->expectException(\RuntimeException::class); diff --git a/tests/Source/Manifest/WritesManifests.php b/tests/Source/Manifest/WritesManifests.php new file mode 100644 index 0000000..121509f --- /dev/null +++ b/tests/Source/Manifest/WritesManifests.php @@ -0,0 +1,61 @@ + */ + private array $manifests = []; + + protected function tearDown(): void + { + foreach ($this->manifests as $path) { + if (is_file($path)) { + unlink($path); + } + } + + $this->manifests = []; + } + + protected static function dataSetContext(string $key): string + { + return \sprintf('https://example.com/%s.jsonld', $key); + } + + /** + * @param list $keys + * + * @return string path to the manifest + */ + protected function manifestFor(array $keys): string + { + $entries = array_map(static fn (string $key): string => \sprintf( + " %s:\n title: %s\n access_url: https://example.com/%s.json\n crs: EPSG:25832\n model: Example\n context_url: %s", + $key, + $key, + $key, + self::dataSetContext($key), + ), $keys); + + $path = tempnam(sys_get_temp_dir(), 'sources-'); + + if (false === $path) { + $this->fail('Could not create a temporary manifest.'); + } + + file_put_contents($path, "sources:\n".implode("\n", $entries)."\n"); + $this->manifests[] = $path; + + return $path; + } +} From 87e540f1914158fbf9913a7005c9b6503827480a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 13:06:53 +0200 Subject: [PATCH 054/101] Minor corrections --- src/Geo/Wgs84Transformer.php | 13 ++++--------- src/Import/Exception/EmptySourceException.php | 4 ---- src/Import/Exception/UnknownSourceException.php | 3 --- src/Import/Exception/UpsertFailedException.php | 3 --- 4 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/Geo/Wgs84Transformer.php b/src/Geo/Wgs84Transformer.php index 9f57abe..19489ba 100644 --- a/src/Geo/Wgs84Transformer.php +++ b/src/Geo/Wgs84Transformer.php @@ -20,10 +20,6 @@ final class Wgs84Transformer /** * PROJ definitions for coordinate systems this application reads. * - * The `towgs84=0,0,0,0,0,0,0` term treats ETRS89 as equivalent to WGS84. - * The two datums have diverged by roughly a metre since 1989; see ADR 004 - * for why that is accepted here. - * * @var array */ private const array DEFINITIONS = [ @@ -58,10 +54,9 @@ public function __construct(array $definitions = []) } /** - * Coordinates are not rounded. Downstream use is unknown and may include - * planning work, so the transformed value is published as computed. + * Coordinates are not rounded. * - * @param string $srid source CRS, e.g. "EPSG:25832" + * @param string $srid source CRS * * @return array{float, float} GeoJSON coordinate order: [longitude, latitude] */ @@ -80,7 +75,7 @@ public function toWgs84(string $srid, float $x, float $y): array } /** - * @param string $srid source CRS, e.g. "EPSG:25832" + * @param string $srid source CRS * * @return array{type: string, coordinates: array{float, float}} GeoJSON Point */ @@ -102,7 +97,7 @@ public function point(string $srid, float $x, float $y): array * A third ordinate (elevation) is dropped: the inputs are two-dimensional, * and vertical datums are a separate concern this class does not model. * - * @param string $srid source CRS, e.g. "EPSG:25832" + * @param string $srid source CRS * @param array $geometry GeoJSON geometry object * * @return array{type: string, coordinates: mixed} diff --git a/src/Import/Exception/EmptySourceException.php b/src/Import/Exception/EmptySourceException.php index 5a87617..d6bcc3f 100644 --- a/src/Import/Exception/EmptySourceException.php +++ b/src/Import/Exception/EmptySourceException.php @@ -6,10 +6,6 @@ /** * A source ran to completion and yielded no entities. - * - * Carries no diagnosis: the source discarded every record through its own - * guards without raising anything, so there is nothing here to report beyond - * which source it was. */ final class EmptySourceException extends \RuntimeException { diff --git a/src/Import/Exception/UnknownSourceException.php b/src/Import/Exception/UnknownSourceException.php index b607c65..c799ba3 100644 --- a/src/Import/Exception/UnknownSourceException.php +++ b/src/Import/Exception/UnknownSourceException.php @@ -6,9 +6,6 @@ /** * No source is registered under the requested key. - * - * Separate from EmptySourceException so a caller can tell a key it can fix - * from a source that ran and had nothing to show. */ final class UnknownSourceException extends \InvalidArgumentException { diff --git a/src/Import/Exception/UpsertFailedException.php b/src/Import/Exception/UpsertFailedException.php index 1e18c24..de80c2a 100644 --- a/src/Import/Exception/UpsertFailedException.php +++ b/src/Import/Exception/UpsertFailedException.php @@ -6,9 +6,6 @@ /** * The payload was built, but the broker could not be written to. - * - * Reusing the underlying message keeps whatever the broker said, and the - * original exception stays available as the cause. */ final class UpsertFailedException extends \RuntimeException { From c05adfedb323e8ee32468cf3e7c37c2ab39106c9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 14:41:32 +0200 Subject: [PATCH 055/101] Removed task broker:entities for now --- .env | 5 ----- README.md | 1 - Taskfile.yml | 8 -------- task/scripts/broker-entities | 34 ---------------------------------- 4 files changed, 48 deletions(-) delete mode 100755 task/scripts/broker-entities diff --git a/.env b/.env index b68e4c9..ee8aa28 100644 --- a/.env +++ b/.env @@ -38,11 +38,6 @@ APP_BROKER_BASE_URI=http://scorpio.local:9090/ # Smart Data Models ones. ENTER_NGSI_CONTEXT_URLS='https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld' -# A single domain context, for the Link header that read requests need. Only -# used by `task broker:entities`, and it must be the context defining the type -# being read — Parking while that is the only model published. -ENTER_NGSI_DOMAIN_CONTEXT=https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld - # Where each feed is read from is not configured here. It belongs to the data # set rather than to the environment, and lives in config/sources.yaml — see # docs/adr/007-source-manifest.md. diff --git a/README.md b/README.md index 3957294..c80ab34 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ source feed (JSON) task import # list the available sources task import -- mtm_spatialmaps-handicap-parking # import one task import -- mtm_spatialmaps-handicap-parking --dry-run --limit 5 # print the payload instead -task broker:entities -- OnStreetParking 10 # read back what landed ``` ### Source manifest diff --git a/Taskfile.yml b/Taskfile.yml index b5cfede..84fbd88 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -40,14 +40,6 @@ tasks: desc: 'Import a source into the broker, e.g. task import -- mtm_spatialmaps-handicap-parking' cmd: ddev console app:import {{.CLI_ARGS}} - broker:entities: - desc: 'List broker entities of a type, e.g. task broker:entities -- OnStreetParking 10' - cmd: >- - ddev exec sh -c - "APP_BROKER_BASE_URI='$APP_BROKER_BASE_URI' - ENTER_NGSI_DOMAIN_CONTEXT='$ENTER_NGSI_DOMAIN_CONTEXT' - sh task/scripts/broker-entities {{.CLI_ARGS}}" - coding-standards:apply: desc: 'Apply coding standards' cmds: diff --git a/task/scripts/broker-entities b/task/scripts/broker-entities deleted file mode 100755 index 280bb6c..0000000 --- a/task/scripts/broker-entities +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -# List entities of a given type from the NGSI-LD broker. -# -# Read requests need a domain context supplied in a Link header; without it the -# broker cannot expand a short type name into the full term the entity was -# written under. The context comes from ENTER_NGSI_DOMAIN_CONTEXT, which must -# be the one defining the type being asked for. -# -# Usage: broker-entities [limit] - -set -eu - -if [ $# -lt 1 ]; then - echo "Usage: broker-entities [limit]" >&2 - echo " NGSI-LD entity type, e.g. OnStreetParking" >&2 - echo " [limit] maximum entities to return (default 100)" >&2 - exit 64 -fi - -TYPE="$1" -LIMIT="${2:-100}" - -RESPONSE=$(curl -sS \ - -H "Link: <${ENTER_NGSI_DOMAIN_CONTEXT}>; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\"" \ - "${APP_BROKER_BASE_URI%/}/ngsi-ld/v1/entities?type=${TYPE}&limit=${LIMIT}") - -printf '%s\n' "$RESPONSE" - -# A misspelled type, or one whose context is not the one configured, returns an -# empty list with HTTP 200 — indistinguishable from an empty broker or a failed -# import. Say what was asked for so the difference is visible. -if [ "$(printf '%s' "$RESPONSE" | tr -d '[:space:]')" = "[]" ]; then - printf '\nNo entities of type "%s". A type that does not match, or one defined in a context other than the configured ENTER_NGSI_DOMAIN_CONTEXT, returns an empty list rather than an error — check both before concluding the import failed.\n' "$TYPE" >&2 -fi From a97f6020b72a719e0c6fc559a9f4a6768e390f76 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 14:42:35 +0200 Subject: [PATCH 056/101] Removed comment from .env --- .env | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.env b/.env index ee8aa28..819e914 100644 --- a/.env +++ b/.env @@ -37,8 +37,4 @@ APP_BROKER_BASE_URI=http://scorpio.local:9090/ # context resolves the NGSI-LD terms and the domain context resolves the # Smart Data Models ones. ENTER_NGSI_CONTEXT_URLS='https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld' - -# Where each feed is read from is not configured here. It belongs to the data -# set rather than to the environment, and lives in config/sources.yaml — see -# docs/adr/007-source-manifest.md. ###< app ### From 346cbc6fec91d661478c46de6fd748ccf6a62903 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 14:43:01 +0200 Subject: [PATCH 057/101] Cut some comments --- src/Source/MtmSpatialMaps/HandicapParking.php | 8 ++------ src/Source/SourceInterface.php | 9 ++++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index b170ff0..1612cf0 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -15,7 +15,6 @@ * Disabled parking bays in Aarhus Municipality. * * @see config/sources.yaml - * @see https://github.com/smart-data-models/dataModel.Parking/tree/master/OnStreetParking */ final readonly class HandicapParking implements SourceInterface { @@ -48,12 +47,12 @@ public function entities(): iterable } /** + * Maps one feed record onto an NgsiEntity. + * * @param array $feature GeoJSON Feature */ private function toEntity(array $feature, Descriptor $source): ?NgsiEntity { - // A Feature keeps its attributes under `properties` and its geometry - // beside them, so neither is at the feature's top level. $row = $feature['properties'] ?? null; $geometry = $feature['geometry'] ?? null; @@ -74,9 +73,6 @@ private function toEntity(array $feature, Descriptor $source): ?NgsiEntity $source->model ); - // `forDisabled` rather than ParkingGroup's `onlyDisabled`: the two - // models have separate category enums, so values are not interchangeable. - // `onStreet` is dropped because the entity type already states it. return $entity ->setProperty('name', $this->address($row)) ->setProperty('description', trim((string) ($row['bemrk'] ?? ''))) diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php index eaea6a7..f985048 100644 --- a/src/Source/SourceInterface.php +++ b/src/Source/SourceInterface.php @@ -8,12 +8,11 @@ use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; /** - * One input data set, converted to NGSI-LD entities. + * Converts one input data set into NGSI-LD entities. * - * Implementations own everything specific to their feed: where it comes from, - * its field names, its quirks, and which Smart Data Model it maps onto. The - * broker and the import stay unaware of all of it, so adding the next ENTER - * data set means adding one class and nothing else. + * An implementation owns its feed's origin, field names, quirks and target + * Smart Data Model. The broker and the import know none of that, so a new + * ENTER data set costs exactly one class. */ #[AutoconfigureTag('app.source')] interface SourceInterface From 3353d0869c965837e8126ab4421e415688a2dea0 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 14:43:28 +0200 Subject: [PATCH 058/101] Coding standards --- src/Import/DataSourceImporter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Import/DataSourceImporter.php b/src/Import/DataSourceImporter.php index 3030870..f2a7fa7 100644 --- a/src/Import/DataSourceImporter.php +++ b/src/Import/DataSourceImporter.php @@ -105,6 +105,7 @@ public function import(string $key, ?int $limit = null): ImportResult * Get list of registered datasets. * * @see config/sources.yaml + * * @return array keyed by source key */ private function registry(): array @@ -119,8 +120,10 @@ private function registry(): array } /** - * Return an array of contexts. Each dataset holds its own context + * Return an array of contexts. Each dataset holds its own context. + * * @see config/sources.yaml + * * @return array */ private function contexts(string $key): array From 388d88a463c5f3125fcb44cc77f5d2d1345f7553 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 14:57:57 +0200 Subject: [PATCH 059/101] ADR shortening --- docs/adr/001-architecture-symfony-docker.md | 75 ++++------ docs/adr/002-publish-to-a-context-broker.md | 120 +++++++-------- docs/adr/003-ngsi-ld-representation.md | 47 +++--- docs/adr/004-coordinate-reference-system.md | 139 ++++++++---------- .../005-smart-data-models-as-vocabulary.md | 134 +++++++---------- .../006-onstreetparking-over-parkinggroup.md | 87 +++++------ 6 files changed, 249 insertions(+), 353 deletions(-) diff --git a/docs/adr/001-architecture-symfony-docker.md b/docs/adr/001-architecture-symfony-docker.md index c0f8708..1431f7b 100644 --- a/docs/adr/001-architecture-symfony-docker.md +++ b/docs/adr/001-architecture-symfony-docker.md @@ -10,61 +10,49 @@ ## Context -The adapter reads open data sets, converts them to a standard smart-city -representation, and publishes them to a context broker. It needs a runtime, an -HTTP client, a console for running imports, and a local development environment -including a broker to import into. It has no web UI and no domain data of its -own. - -ITK Dev maintains a fleet of PHP services with an established Docker-based -development convention, expressed as versioned project templates with shared CI -and coding-standards configuration. A new application either adopts that or -diverges from it. - -This ADR serves to decide the runtime, framework and development -environment the application is built on. +This application reads open data sets, converts them to a standard smart-city +representation, and publishes them to a context broker. The organisation +maintains its PHP services on a versioned Docker template carrying shared CI +and coding-standards configuration. This ADR serves to decide the runtime, +framework and development environment the application is built on. ### Drivers -- **Functional:** scheduled console commands; outbound HTTP; a local broker. - No database and no HTTP surface of its own. -- **Non-functional:** minimal onboarding cost; shared tooling rather than - reimplemented tooling; reproducible across developers and CI; long-term - vendor support. +- **Functional:** scheduled console commands, outbound HTTP, and a local broker + to import into. No database and no HTTP surface of its own. +- **Non-functional:** shared tooling rather than reimplemented tooling, minimal + onboarding, reproducible across developers and CI, long-term vendor support. ### Options Considered -1. **PHP 8.4 / Symfony 8 on the ITK Dev `symfony-8` template.** Matches the - organisation's existing stack, so CI, coding standards and task runner come - for free; the console component suits scheduled imports. Provisions a web - server, database and mail catcher this application never uses, and its PHP - version runs ahead of developer hosts, making containers mandatory. -2. **Minimal framework project without the template, run on the host.** No - unused services, no container requirement for the application — but shared - CI and coding-standards config would be reimplemented by hand, and a local - broker needs containers anyway, so the dependency is moved rather than - removed. -3. **A second entry point in an existing internal application.** One - deployment to operate, but couples a batch importer's release cycle to a - user-facing application and inherits dependencies it has no use for. +1. **PHP 8.4 / Symfony 8 on the maintained template.** CI, coding standards and + task runner come for free, and its console suits scheduled imports; it + provisions services this application never uses, and its PHP runs ahead of + developer hosts. +2. **A minimal project on the host, without the template.** No unused services + and no container requirement, but shared configuration is rebuilt by hand + and a local broker needs containers anyway, moving the requirement rather + than removing it. +3. **A second entry point in an existing internal application.** One deployment + to operate, but couples a batch importer to a user-facing release cycle and + inherits dependencies it has no use for. 4. **A different language ecosystem on a bespoke setup.** Richer geospatial - libraries in some ecosystems, but no internal expertise and no shared - tooling. The transformations needed are available as mature libraries in the - established stack too. + libraries, but no internal expertise and no shared tooling; the needed + transformations exist as mature libraries in the established stack. ## Decision -**PHP 8.4 + Symfony 8** on the ITK Dev `symfony-8` template, as its **own -deployable service**, with a containerised broker overlay for local development. +**PHP 8.4 + Symfony 8** on the ITK Dev Docker template, as its **own deployable +service**, with a containerised broker overlay for local development. - Standardising costs less over the application's lifetime than trimming unused - services. A second toolchain must be learned and patched; idle containers + services: a second toolchain must be learned and patched; idle containers cost only disk. -- A batch importer and a user-facing application have different lifecycles and - failure modes, so they stay separate services. +- A batch importer's lifecycle and failure modes differ from a user-facing + application's, so it stays its own service. - No domain persistence is needed — the broker is the system of record — so the - template's database service is left unused rather than removed, keeping - template updates a clean diff. + template's database is left unused rather than removed, keeping template + updates a clean diff. - Local development includes a real broker, so imports are verified end to end rather than only as serialised output. @@ -78,9 +66,8 @@ deployable service**, with a containerised broker overlay for local development. ### Negative / Trade-offs -- **Containers are mandatory.** The template's PHP runs ahead of developer - hosts, so dependency management, console commands and tests cannot run - natively. Most likely source of first-run confusion. +- Containers are mandatory; dependency management, console commands and tests + cannot run natively. - A web server, database and mail catcher are provisioned and never used. - Broker images are not published for every CPU architecture, so local start-up may be slow under emulation. diff --git a/docs/adr/002-publish-to-a-context-broker.md b/docs/adr/002-publish-to-a-context-broker.md index 7372479..a1225c6 100644 --- a/docs/adr/002-publish-to-a-context-broker.md +++ b/docs/adr/002-publish-to-a-context-broker.md @@ -10,69 +10,54 @@ ## Context -The adapter makes data available to consumers the organisation does -not control and cannot brief. The data already exists in operational systems, -with heterogeneous formats, coordinate systems and access methods. What has to -be decided is the mechanism by which it is published. The source systems remain -authoritative; the published copy is not a system of record. - -This ADR serves to decide the mechanism by which data is published to -consumers. +This application republishes data held in operational systems, in heterogeneous +formats and coordinate systems, to consumers the organisation neither controls +nor can brief. The source systems stay authoritative; the published copy is not +a system of record. This ADR serves to decide the mechanism by which data is +published to consumers. ### Drivers -- **Functional:** query by location and attribute, not bulk download only; - several data sets reaching one consumer-facing surface; change notification; - adding a data set without changing consumer integrations. -- **Non-functional:** interpretable by consumers we have never spoken to; - operational cost proportionate to the data and the number of consumers; - existing client tooling rather than clients we supply; an interface that - outlives individual data sets. +- **Functional:** query by location and attribute rather than bulk download + only, several data sets on one consumer-facing surface, change notification, + and adding a data set without changing consumer integrations. +- **Non-functional:** interpretable by consumers never spoken to, served through + client tooling that already exists, operational cost proportionate to the data + and its consumers, and an interface that outlives individual data sets. ### Options Considered -1. **An NGSI-LD context broker.** Provides geospatial and attribute queries, - pagination, subscriptions and a temporal interface without implementing - them; payloads carry a vocabulary reference, so they are self-describing; - many producers converge on one consumer surface. Substantial operational - weight — typically a database and message bus alongside the broker — and the - strictness of a particular implementation is inherited. -2. **Static file export** on a web server or object store. Near-zero - operational cost, trivially cacheable, readable by anything. No query, so - consumers download everything and filter client-side; no change - notification; conventions must be documented in prose because nothing in the - file declares its own meaning. -3. **A bespoke REST API** over our own datastore. Exact fit, full control of - the query surface and semantics. Every capability is ours to build and - maintain — geo-queries, filtering, pagination, notifications, documentation, - clients, versioning — and consumers must learn an interface that exists - nowhere else. -4. **Direct database access or a read replica.** No API layer, powerful ad-hoc - querying. Exposes internal schema as a public contract, requires per-consumer +1. **An NGSI-LD context broker.** Geospatial and attribute queries, pagination, + subscriptions and a temporal interface without implementing them, and + payloads carrying a vocabulary reference; substantial operational weight, and + the strictness of a particular implementation is inherited. +2. **A bespoke REST API** over a datastore of our own. Exact fit and full + control of the query surface and its semantics; every capability from + geo-queries to notifications, clients and versioning is ours to build, and + consumers must learn an interface that exists nowhere else. +3. **Direct database access or a read replica.** No API layer, powerful ad-hoc + querying; exposes internal schema as a public contract, needs per-consumer credentials and network access, and is unusable by browser-based consumers. ## Decision Publish to an **NGSI-LD context broker**. -- Consumers of geographic data need "everything within this area" and - "everything of this kind" more often than the whole data set. A broker - provides that as a standard interface rather than a per-data-set feature. -- Publishing structure without a vocabulary reference requires every consumer - to have our documentation. A broker payload carries the reference. -- For a single small data set a static export would be cheaper and better. Once - several heterogeneous data sets must be published, the fixed operational cost - is paid once while the per-data-set cost approaches zero, and consumers - integrate once rather than once per source. -- New consumers require no change to the adapter, and new data sets require no - change to consumers. -- Existing viewers, dashboards and connectors speak this interface; a bespoke - API would mean supplying clients indefinitely. - -The broker's value is interoperability and query, not storage. If no consumer -reads the data through its interface, a static export would have been the better -decision. Revisit once data sets have been published long enough for consumers -to appear. +1. Consumers of geographic data ask for an area or a kind more often than for a + whole data set, and a broker offers that as a standard interface rather than + a per-data-set feature. +2. Structure published without a vocabulary reference obliges every consumer to + hold separate documentation; a broker payload carries the reference. +3. A single small data set would be served better by a static export. Across + several heterogeneous ones the fixed operational cost is paid once, the + per-data-set cost approaches zero, and consumers integrate once rather than + once per source. +4. New consumers require no change here, new data sets none from consumers, and + existing viewers, dashboards and connectors already speak this interface. + +The value taken is interoperability and query, not storage. Revisit once +consumers have had time to appear: if none read the data through the interface, +a static export was the better decision. ## Consequences @@ -80,25 +65,22 @@ to appear. - Geospatial and attribute queries, pagination, subscriptions and a temporal interface, none of which are implemented here. -- Payloads reference a shared vocabulary, so they need no bespoke - documentation. +- Payloads reference a shared vocabulary, so they need no bespoke documentation. - Additional data sets reach every existing consumer with no integration work. -- The adapter has no database and no read surface of its own. +- This application keeps no database and no read surface of its own. ### Negative / Trade-offs -- **Operational weight out of proportion to a small data set.** A broker - deployment is several services to run, patch, monitor and back up. For a - static data set a file on a web server would serve the same need. -- **Broker implementations impose constraints beyond the standard.** Those - encountered include accepting only one spelling of a UTC timestamp while - rejecting an equivalent one, requiring the vocabulary reference on read - requests — with omission returning an empty success rather than an error — - and collapsing single-element lists to scalars. -- **Vocabulary documents may be fetched over the network during writes**, so - third-party availability becomes part of the import path. -- **No delete semantics.** Upsert creates and updates but never removes, so - records that disappear upstream persist until reconciliation is built. -- JSON-LD is a learning curve for maintainers and consumers. -- Fitting data to a shared vocabulary costs effort that publishing as-is - would not. +- Operational weight out of proportion to a small data set: several services to + run, patch, monitor and back up. +- Broker implementations impose constraints beyond the standard. Those met + include one accepted spelling of a UTC timestamp while an equivalent is + rejected, the vocabulary reference required on reads where omission returns an + empty success rather than an error, and single-element lists collapsed to + scalars. +- Vocabulary documents may be fetched over the network during writes, making + third-party availability part of the import path. +- Upsert creates and updates but never removes, so records that disappear + upstream persist until reconciliation is built. +- JSON-LD is a learning curve for maintainers and consumers, and fitting data to + a shared vocabulary costs effort that publishing as-is would not. diff --git a/docs/adr/003-ngsi-ld-representation.md b/docs/adr/003-ngsi-ld-representation.md index 8adcb7e..ffbb16f 100644 --- a/docs/adr/003-ngsi-ld-representation.md +++ b/docs/adr/003-ngsi-ld-representation.md @@ -10,57 +10,52 @@ ## Context -ADR 002 chooses an NGSI-LD context broker. That leaves two representation -choices open: how attributes are shaped, and which write operation is used. +ADR 002 chooses an NGSI-LD context broker, which leaves the representation +open. -NGSI v2, the older FIWARE API generation, was not considered viable: it has no -`@context`, so a shared vocabulary cannot be expressed, and the -organisation operates no v2 broker. - -This ADR serves to decide how attributes are shaped and which write -operation is used. +This ADR serves to decide how attributes are shaped and which write operation +is used. ### Drivers -- **Functional:** the broker must accept the payload on write; attribute-level - metadata must be expressible; repeated imports must not duplicate entities. -- **Non-functional:** idempotency; payload size; readability for consumers. +- **Functional:** the broker must accept the payload on write, attribute + metadata must be expressible, and repeated imports must not duplicate + entities. +- **Non-functional:** idempotency, payload size, readability for consumers. ### Options Considered #### Attribute form -1. **Normalized** — every attribute an object naming its kind. Verbose, but - the form brokers accept on write and the only one carrying metadata such as - `observedAt`. -2. **Key-values** — flat `name: value`. Much smaller and easier to read, but - read-only and cannot carry attribute metadata. +1. **Normalized.** Every attribute an object naming its kind. Verbose, but the + form brokers accept on write and the only one carrying metadata. +2. **Key-values.** Flat `name: value`, smaller and easier to read, but + read-only and unable to carry attribute metadata. #### Write operation -1. **Batch upsert** — creates or updates. Idempotent when identifiers are - derived from source keys. -2. **Create** — fails with `409` for identifiers that already exist, so a +1. **Batch upsert.** Creates or updates, so a re-import refreshes in place. +2. **Create.** Fails with `409` for an identifier that already exists, so a re-import errors rather than refreshing. -3. **Batch replace** — silently drops attributes absent from the payload, - making partial payloads destructive. +3. **Batch replace.** Silently drops attributes absent from the payload, + making a partial payload destructive. ## Decision Publish **normalized** NGSI-LD with `Content-Type: application/ld+json`, via **batch upsert**. -- Normalized is the only form accepted on write, so key-values is not a real - option for a producer. -- Upsert makes imports idempotent: identifiers derive from each source's - primary key, so re-running updates in place. +- Normalized is the only form accepted on write, so key-values is not + available to a producer. +- Identifiers derive from each data set's primary key, so upsert makes a + re-run update in place rather than add. ## Consequences ### Positive - Re-imports produce no duplicates and need no prior state. -- Attribute metadata remains available if a source ever supplies it. +- Attribute metadata stays expressible where a data set supplies it. ### Negative / Trade-offs diff --git a/docs/adr/004-coordinate-reference-system.md b/docs/adr/004-coordinate-reference-system.md index 177d6b7..f77f88a 100644 --- a/docs/adr/004-coordinate-reference-system.md +++ b/docs/adr/004-coordinate-reference-system.md @@ -11,102 +11,83 @@ ## Context Every entity the adapter publishes carries a `location` GeoProperty, so the -coordinate reference system is a cross-cutting concern rather than a per-source -detail. +coordinate reference system is cross-cutting rather than a per-input detail. +Inputs arrive in whatever CRS their publisher uses — Danish municipal data is +commonly projected, typically EPSG:25832 (ETRS89 / UTM zone 32N) as eastings and +northings in metres — and no single input CRS can be assumed. A GeoJSON envelope +states a geometry type but not units, so projected coordinates arrive inside one +undetected. -Input data arrives in whatever CRS its publisher uses. Danish municipal data is -commonly projected — typically EPSG:25832 (ETRS89 / UTM zone 32N), as eastings -and northings in metres. Other inputs may already be geographic, or use a -different projection. The adapter cannot assume one input CRS. - -Projected coordinates are sometimes delivered inside a GeoJSON envelope, which -states a geometry type but not units. The data models specify only that -`location` is GeoJSON and make no reference to a coordinate system; the -constraint comes from GeoJSON itself. - -This ADR serves to decide which coordinate reference system is published, -and at what precision. +This ADR serves to decide which coordinate reference system is published, and at +what precision. ### Drivers -- **Functional:** consumers must be able to interpret `location` unbriefed; - geo-queries must return correct results; clients must render without - preprocessing; one rule must hold for every input. -- **Non-functional:** self-description; conformance; uniformity across inputs; - precision no worse than the input. +- **Functional:** `location` is interpretable unbriefed, geo-queries return + correct results, and clients render without preprocessing. +- **Non-functional:** conformance and self-description; one rule for every + input; precision no worse than the input. ### Options Considered -1. **Normalise everything to WGS84, reprojecting in the adapter.** Conforms to - RFC 7946; self-describing; geo-queries work; one rule however many input - CRSs accumulate. Requires a reprojection dependency, and each input must - declare its CRS. -2. **Pass each input's native CRS through unchanged.** No transformation, no - dependency — but produces invalid GeoJSON with nowhere to declare the CRS, - makes entities from different inputs mutually incomparable, and breaks - geo-queries because distances are read as degrees. Every failure is silent. -3. **Publish WGS84 and also retain original coordinates in an extra - attribute.** Avoids a round trip for consumers wanting native coordinates, - but the attribute cannot have a stable shape: each input brings its own CRS - and geometry type, and it would be absent for inputs already in WGS84. A - consumer cannot code against that, so it would go unused. -4. **Pass native CRSs through under RFC 7946's "prior arrangement" clause, - documenting each out of band.** Permitted by the RFC, but the clause - requires all parties to have agreed — incompatible with a broker whose - consumers are unknown by design. +1. **Normalise everything to WGS84, reprojecting in the adapter.** One rule + however many input CRSs accumulate. Needs a reprojection dependency, and each + input must declare its CRS. +2. **Pass each input's native CRS through unchanged.** No transformation and no + dependency, but the GeoJSON is invalid, entities from different inputs are + incomparable, and distances read as degrees. +3. **Publish WGS84 and also keep the original coordinates in an extra + attribute.** Saves consumers a round trip, but the attribute has no stable + shape — CRS and geometry type differ per input, and it is absent for inputs + already in WGS84 — so nothing can be coded against it. +4. **Pass native CRSs through under RFC 7946's "prior arrangement" clause.** + Permitted, but the clause requires all parties to have agreed, which a broker + whose consumers are unknown by design cannot satisfy. ## Decision -Every `location` the adapter emits is **EPSG:4326 (WGS84) -longitude/latitude**, at **full precision — coordinates are not rounded**. Each -input declares its own CRS; reprojection happens at the boundary between reading -an input and building an entity, and nowhere else. - -- **The specification is unambiguous.** RFC 7946 §4: "The coordinate reference - system for all GeoJSON coordinates is a geographic coordinate reference - system, using the World Geodetic System 1984 (WGS 84) datum, with longitude - and latitude units of decimal degrees." NGSI-LD GeoProperty values are - GeoJSON, so the requirement is inherited. -- **There is no way to declare otherwise.** RFC 7946 Appendix B.1: - "Specification of coordinate reference systems has been removed, i.e., the - 'crs' member of [GJ2008] is no longer used." Publishing projected coordinates - means publishing an undeclarable assumption. Inputs may still carry that - deprecated member; it can be read, but not passed on. -- Entities from different inputs are queried together, so a query spanning two - inputs published in different CRSs returns meaningless results. -- A broker given projected coordinates accepts them, answers geo-queries - incorrectly, and renders points in the wrong location. No error is raised at - any stage. -- **The conversion is lossless at full precision.** A projected-to-geographic - round trip returns the input exactly when no rounding is applied. Rounding - trades accuracy for a marginal reduction in payload size. -- `source` and `seeAlso` can reference the originating export, which states its - own CRS. A coordinate copied into an extra attribute states nothing. +Every `location` the adapter emits is **EPSG:4326 (WGS84) longitude/latitude**, +at **full precision — coordinates are not rounded**. Each input declares its own +CRS; reprojection happens at the boundary between reading an input and building +an entity, and nowhere else. + +1. **The specification leaves no choice.** RFC 7946 §4 fixes the CRS for all + GeoJSON coordinates as geographic, WGS 84 datum, longitude and latitude in + decimal degrees; NGSI-LD GeoProperty values are GeoJSON and inherit it. +2. **There is no way to declare otherwise.** RFC 7946 Appendix B.1 removed the + `crs` member of the 2008 specification, so projected coordinates publish an + undeclarable assumption. An input may still carry that deprecated member; it + can be read, not passed on. +3. Nothing catches the mistake: a broker accepts projected coordinates, answers + geo-queries incorrectly and places points wrongly, raising no error; and + entities from different inputs are queried together, so a query spanning two + CRSs returns meaningless results. +4. **A round trip is lossless at full precision.** Projected to geographic and + back returns the input exactly when nothing is rounded; rounding trades + accuracy for a marginal payload reduction. +5. `source` and `seeAlso` can reference the originating export, which states its + own CRS. ## Consequences ### Positive -- Payloads are valid GeoJSON and NGSI-LD; geo-queries work and are comparable - across inputs; any client renders them unmodified. -- One rule for every present and future input. -- Reprojection is isolated in one component with its own tests, verified - against independently known reference coordinates, so a regression fails - loudly instead of silently relocating data. +- Payloads are valid GeoJSON and NGSI-LD, comparable across inputs, and render + in any client unmodified, under one rule for every present and future input. +- Reprojection is isolated in one tested component, verified against + independently known reference coordinates, so a regression fails loudly rather + than silently relocating data. ### Negative / Trade-offs -- Adds a reprojection dependency. National grid definitions are not always - shipped and may need registering explicitly, making them load-bearing - project code. -- **Datum shifts are approximated.** ETRS89-based grids are treated as - equivalent to WGS84 via a null datum transformation. The two were coincident - in 1989 and have diverged by roughly 0.5–1 m since, at about 2.5 cm per year. - What is published is therefore ETRS89 labelled WGS84. This is standard - practice in web GIS, but it is the largest error in the pipeline — greater - than the source's own positional accuracy — so a consumer using a rigorous - transformation with an explicit epoch will land about a metre away. +- Adds a reprojection dependency, and national grid definitions are not always + shipped, so registering them becomes load-bearing project code. +- Datum shifts are approximated: ETRS89-based grids are treated as equivalent + to WGS84 via a null datum transformation, so what is published is ETRS89 + labelled WGS84. Coincident in 1989, the two have diverged by roughly 0.5–1 m + at about 2.5 cm per year — standard practice in web GIS, and the largest + error introduced, so a consumer transforming rigorously with an explicit + epoch lands about a metre away. - Consumers with natively projected stacks must convert. - Every new input must declare its CRS, and unsupported ones need adding. -- Only point geometries were implemented initially; line and area geometries - were added later. +- Each geometry type needs its own reprojection, not points alone. diff --git a/docs/adr/005-smart-data-models-as-vocabulary.md b/docs/adr/005-smart-data-models-as-vocabulary.md index 42f11ec..7cba550 100644 --- a/docs/adr/005-smart-data-models-as-vocabulary.md +++ b/docs/adr/005-smart-data-models-as-vocabulary.md @@ -10,117 +10,87 @@ ## Context -NGSI-LD defines how attributes are carried and how to reference a vocabulary. -It does not define entity types or attribute names. Without a vocabulary the -JSON-LD context resolves to nothing, entity types are local strings, and -consumers still need our documentation to interpret anything. - -The choice also determines the cost of onboarding each data set, since mapping -a source onto an existing model takes more effort than exposing its fields -verbatim. - -This ADR serves to decide which vocabulary supplies entity types and +NGSI-LD defines how attributes are carried and how a vocabulary is referenced, +not which entity types and attribute names exist. Without one the JSON-LD +context resolves to nothing and consumers need our documentation to interpret +anything. This ADR serves to decide which vocabulary supplies entity types and attribute names, and the rules for mapping sources onto it. ### Drivers -- **Functional:** types and attributes interpretable without our - documentation; expressible as a JSON-LD context a broker can resolve; - coverage across the domains in scope. -- **Non-functional:** a vocabulary consumers plausibly already know; governed - and maintained by someone else; mapping cost that does not dominate - onboarding. +- **Functional:** types and attributes interpretable without our documentation, + expressible as a JSON-LD context a broker can resolve, covering the domains in + scope. +- **Non-functional:** a vocabulary consumers plausibly already know, maintained + by someone else, at a mapping cost that does not dominate onboarding. ### Options Considered -1. **Smart Data Models.** Purpose-built for NGSI-LD and the reference - vocabulary of that ecosystem; publishes JSON-LD context documents per - domain; broad coverage; each model ships a JSON schema and examples, giving - an objective conformance target; open governance. Model depth varies; - many models assume real-time sensing, so static inventory leaves attributes - unset; required attributes occasionally presuppose a hierarchy the source - lacks; enum spellings sometimes disagree between a model's schema and its - examples; versioning is loose. -2. **A vocabulary of our own, with self-hosted context documents.** Exact fit, - no required attributes we cannot satisfy, full control of naming and - versioning. Nobody else speaks it, so consumers return to reading our - documentation; governance, documentation and versioning become ours - indefinitely; no existing tooling recognises the types. -3. **A general-purpose web vocabulary.** Widely recognised, stable governance, - adequate for names, addresses and descriptions. No NGSI-LD conventions for - geometry or relationships, and no domain-specific terms, so the domains in - scope would remain unmodelled. +1. **Smart Data Models.** The openly governed reference vocabulary of the + NGSI-LD ecosystem, with broad coverage, per-domain context documents, and a + schema and examples per model to conform against. Depth varies, many models + assume real-time sensing, required attributes can presuppose a hierarchy a + source lacks, and versioning is loose. +2. **A vocabulary of our own, with self-hosted context documents.** Exact fit + and full control of naming and versioning, but nobody else speaks it, + governance and documentation stay ours indefinitely, and no existing tooling + recognises the types. +3. **A general-purpose web vocabulary.** Widely recognised and stably governed, + adequate for names, addresses and descriptions, but with no NGSI-LD + conventions for geometry or relationships and no domain terms, leaving the + domains in scope unmodelled. ## Decision Adopt **Smart Data Models**, referencing the relevant domain context documents -alongside the NGSI-LD core context. - -Two rules follow, and they matter more than the choice itself: +alongside the NGSI-LD core context. Which model a given data set uses is +recorded in its own ADR; this one states policy. 1. **Use an existing model; do not invent a type.** An imperfect standard type - is more useful to a consumer than a perfect private one. + is more useful to a consumer than a precise private one. 2. **Never fabricate a value to satisfy a model.** An absent attribute is - honest; a fabricated one is indistinguishable from a measured one. Applied: - + honest; a fabricated one is indistinguishable from a measured one. - Attributes the source cannot fill are left unset, not approximated, defaulted or inferred. - Where a model *requires* an attribute the source cannot supply, choose a - different model rather than inventing the value. Required relationships - are the common case: inventing a related entity yields something - schema-valid and factually wrong that must then be maintained - indefinitely. A sibling model without the requirement is the better - choice even if its terms are less precise. - - In a hierarchy — a root site, subdivisions beneath it, individual units - beneath those, each lower level requiring a relationship upward — publish - at the highest level the source can populate. Static inventory typically - describes a location and a count of units without describing what the - location is part of, so the site level is usually correct. - -The concrete model chosen for a given data set is recorded in its own ADR; this -one states policy only. + sibling model without the requirement, even if its terms are less precise. + - In a hierarchy where each level requires a relationship upward, publish at + the highest level the source can populate — typically the top, for a data + set giving a location and a count of units within it. Rationale: -- The context must resolve to terms a consumer recognises, or publishing gains - nothing over a file. -- Smart Data Models is the vocabulary the surrounding ecosystem uses and ships - the context documents needed to reference it, so adoption is a URL rather - than a project. +- A context must resolve to terms a consumer recognises, or publishing gains + nothing over a file, and Smart Data Models ships those documents, so adoption + is a URL rather than a project. - Shipped schemas and examples make modelling disagreements checkable against a specification instead of settled by preference. -- Mandatory relationships propagate downward: choosing a subdivision level - schedules the need for a parent rather than avoiding it, because the - individual-unit level requires a site as well. -- The costs are asymmetric. Publishing at site level and later finding real - sites exist means a one-off migration. Publishing at subdivision level and - never acquiring real sites means maintaining an invented entity - indefinitely, with every consumer that follows the relationship receiving - something meaningless. +- Required relationships propagate downward, so publishing below the top level + defers the need for a parent rather than avoiding it, and the costs are + asymmetric: acquiring real parents later is a one-off migration, whereas never + acquiring them means maintaining an invented entity that every consumer + following the relationship receives as meaningless. ## Consequences ### Positive -- Types and attributes resolve to shared global identifiers. -- Consumers may already have code for the types published. -- Modelling decisions have an external reference point. -- Later data sets are likely already covered, so onboarding does not start with - vocabulary design. -- Published entities are self-contained, with nothing invented to keep in sync. -- Finer granularity can be added later beneath what is already published. +- Types and attributes resolve to shared global identifiers consumers may + already have code for. +- A later data set is likely covered already, so onboarding it does not start + with vocabulary design. +- Published entities are self-contained, with nothing invented to keep in sync, + and finer granularity can be added beneath them later. ### Negative / Trade-offs -- **Many attributes will always be empty.** Models built around real-time - sensing carry availability, occupancy and detection attributes that static - inventory cannot fill. -- **Model choice is embedded in entity identifiers.** Changing model later - means deleting and re-publishing rather than updating in place, so selection - deserves attention before a data set is first published. -- **Enum values must be read from the schema, not the examples.** Where the two - disagree the schema is authoritative, and equivalent-looking values differ - between sibling models, so they must not be copied across. -- **Loose versioning.** A model can change without an obvious signal. +- Models built around real-time sensing carry attributes static inventory + cannot fill, so many will always be empty. +- Model choice is embedded in entity identifiers, so changing model later means + deleting and re-publishing rather than updating in place. +- Enum values must be read from the schema rather than the examples: where the + two disagree the schema is authoritative, and equivalent-looking values differ + between sibling models, so they cannot be copied across. +- Versioning is loose, so a model can change without an obvious signal. - Mapping a source to a model takes longer than exposing its fields verbatim, and occasionally the fit is poor. diff --git a/docs/adr/006-onstreetparking-over-parkinggroup.md b/docs/adr/006-onstreetparking-over-parkinggroup.md index 8b04272..f72fe3f 100644 --- a/docs/adr/006-onstreetparking-over-parkinggroup.md +++ b/docs/adr/006-onstreetparking-over-parkinggroup.md @@ -10,54 +10,42 @@ ## Context -The parking domain is organised as a hierarchy: - -```text -OnStreetParking / OffStreetParking site — no parent, requires id, type, location - └── ParkingGroup subdivision — requires refParkingSite - └── ParkingSpot individual unit — requires refParkingSite, status, category -``` +The parking domain is a hierarchy. A site — `OnStreetParking` or +`OffStreetParking` — requires only `id`, `type` and `location`. Below it, a +`ParkingGroup` subdivision and a `ParkingSpot` unit must each reference a site, +and a spot also requires `status` and `category`. This decision applies where a data set provides a count of units per location with a point geometry, no reference to a containing site, and neither per-unit geometry nor occupancy. -Entity identifiers embed the type by convention, so the choice must be made -before first publication: changing it afterwards means deleting and -re-publishing. - This ADR serves to decide which model in the parking hierarchy is published under those conditions. ### Drivers -- **Functional:** every mandatory relationship must point at an entity that - exists; the restriction on who may park must be expressible unambiguously; - finer granularity addable later without restructuring what is published. -- **Non-functional:** nothing invented purely to satisfy a schema; a choice - that is cheap to reverse in preference to one that is not. +- **Functional:** every mandatory relationship points at an entity that exists; + the restriction on who may park is expressible unambiguously; finer + granularity is addable later without restructuring. +- **Non-functional:** nothing invented purely to satisfy a schema; a reversible + choice in preference to one that is not. ### Options Considered -1. **`ParkingGroup`, creating the missing parent site.** `category` offers - `onlyDisabled`, which by name states exclusivity, and the model's reference - example for disabled parking sits at this level. But `refParkingSite` is - mandatory and no value is available for it, so a parent must be invented; - one spanning the whole administrative area asserts a false containment, and - its own mandatory geometry would carry no meaning. - `ParkingSpot` also requires a site, so adding per-unit data later would force - the invented entity into existence after entities had been published against - it. +1. **`ParkingGroup`, creating the missing parent site.** `onlyDisabled` states + exclusivity by name, and the model's reference example for disabled parking + sits at this level — but points at a real street-address site. + `refParkingSite` is mandatory with no value available, so a parent must be + invented; one spanning the administrative area asserts a false containment + and its own mandatory geometry carries no meaning. 2. **`ParkingGroup`, omitting `refParkingSite`.** Nothing invented, smallest change — but knowingly non-conformant, and a schema validator flags every entity. -3. **`OnStreetParking`.** Requires only `id`, `type` and `location`, all of - which are available. It is the entity both `ParkingGroup` and - `ParkingSpot` are required to reference, so finer granularity can be - attached beneath it, and migration down to `ParkingGroup` stays possible if - real site data appears. `category` offers only `forDisabled`, which does not - state exclusivity as plainly. -4. **`ParkingSpot`.** Models an individual unit, but only a count per location +3. **`OnStreetParking`.** Everything it requires is available, and it is the + entity both lower levels must reference, so granularity can be added beneath + it and migration downward stays possible if real sites appear. `category` + offers only `forDisabled`, which states exclusivity less plainly. +4. **`ParkingSpot`.** Models the individual unit, but only a count per location is available, `status` is mandatory with no occupancy data, and a parent site is required. Rejected outright. @@ -66,39 +54,32 @@ under those conditions. Publish each record as an **`OnStreetParking`** entity, with `category: ["forDisabled"]` and no `refParkingSite`. -- It is the only option that invents nothing; everything the model requires is - available. -- Mandatory relationships propagate downward, so choosing `ParkingGroup` would - schedule the invented parent rather than avoid it — `ParkingSpot` requires a - site too. +- The only option that invents nothing: everything it requires is available. +- Mandatory references propagate downward, so `ParkingGroup` would schedule the + invented parent rather than avoid it — `ParkingSpot` requires a site too. - The costs are asymmetric. Site level, then finding real sites exist, is a - one-off migration. Subdivision level, then never acquiring real sites, means + one-off migration; subdivision level, then never acquiring real sites, means maintaining an invented entity indefinitely. -The model's reference example does use `ParkingGroup` for disabled parking, but -points at a real street-address site. It shows what to do when a site exists, -not when none does. - ## Consequences ### Positive -- No dangling relationship; every entity is self-contained. -- Nothing invented to create, document or keep in sync. +- No dangling relationship: every entity is self-contained, and nothing invented + has to be created, documented or kept in sync. - Conformant to the model's schema without exceptions. - `ParkingGroup` or `ParkingSpot` entities can be attached beneath these later without changing them. ### Negative / Trade-offs -- **`forDisabled` does not state exclusivity.** The schema documents `category` - only as "Street parking category" with an enum list and defines no individual - value. The two models' enums use inconsistent prefixes for what appear to be - the same concepts — `forDisabled` / `forResidents` against `onlyDisabled` / - `onlyResidents` — while both carry `onlyWithPermit`, so the spelling cannot - be relied on to carry exclusivity. -- Values must not be copied between the two models' `category` enums. -- Entity type is embedded in identifiers, so any later change means deleting - and re-publishing. +- `forDisabled` does not state exclusivity. The schema documents `category` only + as "Street parking category" with an enum list and defines no individual + value, and the two models prefix apparently identical concepts inconsistently + — `forDisabled` / `forResidents` against `onlyDisabled` / `onlyResidents` — + while both carry `onlyWithPermit`, so values must not be copied between the + enums. +- Entity type is embedded in identifiers, so any later change means deleting and + re-publishing. - Domain experts may find a single address described as a "site" counter-intuitive. From ca6cc618a7b0048bf3d2678c6c527fc16b731172 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 9 Sep 2026 11:39:25 +0200 Subject: [PATCH 060/101] Refactored source definition --- CHANGELOG.md | 2 + README.md | 46 ++++++++++-- phpstan.dist.neon | 2 + src/Command/SourceListCommand.php | 28 +++++++ src/Command/SourceReadCommand.php | 26 +++++++ src/Command/SourceShowCommand.php | 25 +++++++ src/Import/DataSourceImporter.php | 2 +- src/Source/AbstractSource.php | 73 +++++++++++++++++++ src/Source/MtmSpatialMaps/HandicapParking.php | 32 ++++++-- src/Source/SourceInterface.php | 67 ++++++++++++++++- src/SourceManager.php | 61 ++++++++++++++++ .../SourceArgumentValueResolver.php | 34 +++++++++ tests/Command/ImportCommandTest.php | 2 +- tests/Import/DataSourceImporterTest.php | 2 +- 14 files changed, 381 insertions(+), 21 deletions(-) create mode 100644 src/Command/SourceListCommand.php create mode 100644 src/Command/SourceReadCommand.php create mode 100644 src/Command/SourceShowCommand.php create mode 100644 src/Source/AbstractSource.php create mode 100644 src/SourceManager.php create mode 100644 src/ValueResolver/SourceArgumentValueResolver.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e1c78..c67b120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-7](https://github.com/itk-dev/enter/pull/7) + Refactored source definition * [#3](https://github.com/itk-dev/enter/pull/3) * Import command that reads a geospatial feed, reprojects it to WGS84 and upserts it to an NGSI-LD broker. * A committed record per data set — feed, CRS, model, DCAT-AP metadata. diff --git a/README.md b/README.md index c80ab34..4a3f4ad 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,47 @@ task import -- mtm_spatialmaps-handicap-parking # import task import -- mtm_spatialmaps-handicap-parking --dry-run --limit 5 # print the payload instead ``` -### Source manifest +### Sources + +Adding a data source means adding a [`SourceInterface`](src/Source/SourceInterface.php) implementation. The easiest way +to do this is by extending [`AbstractSource`](src/Source/AbstractSource.php), e.g.: + +```php +getSources(); + + $io->writeln(sprintf('#sources: %d', \count($sources))); + foreach ($sources as $source) { + $io->writeln((string) $source); + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/SourceReadCommand.php b/src/Command/SourceReadCommand.php new file mode 100644 index 0000000..cbbaba6 --- /dev/null +++ b/src/Command/SourceReadCommand.php @@ -0,0 +1,26 @@ +getReader($source); + // $reader->read($source); + // … + } +} diff --git a/src/Command/SourceShowCommand.php b/src/Command/SourceShowCommand.php new file mode 100644 index 0000000..930b974 --- /dev/null +++ b/src/Command/SourceShowCommand.php @@ -0,0 +1,25 @@ +writeln(Yaml::dump($source->toArray(), PHP_INT_MAX)); + + return Command::SUCCESS; + } +} diff --git a/src/Import/DataSourceImporter.php b/src/Import/DataSourceImporter.php index f2a7fa7..597646b 100644 --- a/src/Import/DataSourceImporter.php +++ b/src/Import/DataSourceImporter.php @@ -113,7 +113,7 @@ private function registry(): array $registry = []; foreach ($this->sources as $source) { - $registry[$source->key()] = $source; + $registry[$source->id] = $source; } return $registry; diff --git a/src/Source/AbstractSource.php b/src/Source/AbstractSource.php new file mode 100644 index 0000000..4f655ed --- /dev/null +++ b/src/Source/AbstractSource.php @@ -0,0 +1,73 @@ + + */ + public array $omittedFields = [], + ) { + } + + public function key(): string + { + return $this->id; + } + + public function __toString(): string + { + return sprintf('%s (%s)', $this->title, $this->id); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'publisher' => $this->publisher, + 'contact' => $this->contact, + 'landing_page' => $this->landingPage, + 'access_url' => $this->accessUrl, + 'media_type' => $this->mediaType, + 'crs' => $this->crs, + 'model' => $this->model, + 'context_url' => $this->contextUrl, + 'update_frequency' => $this->updateFrequency, + 'licence' => $this->licence, + 'omitted_fields' => $this->omittedFields, + ]; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 1612cf0..3ea503c 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -6,17 +6,15 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; +use App\Source\AbstractSource; use App\Source\DataSourceReader; use App\Source\Manifest\Catalog; use App\Source\Manifest\Descriptor; -use App\Source\SourceInterface; /** * Disabled parking bays in Aarhus Municipality. - * - * @see config/sources.yaml */ -final readonly class HandicapParking implements SourceInterface +final readonly class HandicapParking extends AbstractSource { private const string KEY = 'mtm_spatialmaps-handicap-parking'; @@ -25,11 +23,29 @@ public function __construct( private Wgs84Transformer $transformer, private Catalog $catalog, ) { - } + parent::__construct( + id: 'mtm_spatialmaps-handicap-parking', + title: 'Handicapparkering, Aarhus Kommune', + description: 'Disabled parking bays in Aarhus Municipality, with the number of reserved bays per location.', + publisher: 'Aarhus Kommune', + contact: 'ppg@aarhus.dk', + landingPage: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune', + accessUrl: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', + mediaType: 'application/geo+json', + crs: 'EPSG:25832', + model: 'OnStreetParking', + contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', + updateFrequency: 'continuous', - public function key(): string - { - return self::KEY; + omittedFields: [ + 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', + 'oprettet_af' => 'Directory username of the municipal employee who created the record.', + 'rettet_af' => 'Directory username of the municipal employee who last edited the record.', + 'oprettet_dato' => 'Describes the register record.', + 'rettet_dato' => 'Describes the register record.', + 'mi_style' => 'MapInfo rendering style, empty throughout the export.', + ], + ); } public function entities(): iterable diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php index f985048..1d6d68d 100644 --- a/src/Source/SourceInterface.php +++ b/src/Source/SourceInterface.php @@ -15,15 +15,76 @@ * ENTER data set costs exactly one class. */ #[AutoconfigureTag('app.source')] -interface SourceInterface +interface SourceInterface extends \Stringable, \JsonSerializable { + public string $id { + get; + } + + public string $title { + get; + } + public string $description { + get; + } + public string $publisher { + get; + } + public string $contact { + get; + } + public string $landingPage { + get; + } + + // @todo access_url? What access? Isn't it just a URL? + public string $accessUrl { + get; + } + + public string $mediaType { + get; + } + + public string $crs { + get; + } + + public string $model { + get; + } + + public string $contextUrl { + get; + } + + public string $updateFrequency { + get; + } + + public ?string $licence { + get; + } + + // @todo What does this mean? + // Fields the feed carries that are not published. Recorded here because + // the source class shows what is mapped but cannot show what was left + // out, or why. /** - * Unique identifier for this source. + * @var array */ - public function key(): string; + public array $omittedFields { + get; + } /** * @return iterable */ + // We should let the (data) source reader read. public function entities(): iterable; + + /** + * @return array + */ + public function toArray(): array; } diff --git a/src/SourceManager.php b/src/SourceManager.php new file mode 100644 index 0000000..681ed07 --- /dev/null +++ b/src/SourceManager.php @@ -0,0 +1,61 @@ + + */ + private array $indexed; + + /** + * @param iterable $sources + */ + public function __construct( + #[AutowireIterator('app.source')] + private readonly iterable $sources, + ) { + } + + /** + * Get sources indexed by ID. + * + * @return array + */ + public function getSources(): array + { + if (!isset($this->indexed)) { + $sources = []; + foreach ($this->sources as $source) { + if (!$source instanceof SourceInterface) { + throw new \InvalidArgumentException(sprintf('Invalid source class: %s (must extend %s)', $source::class, AbstractSource::class)); + } + $id = $source->id; + if (isset($sources[$id])) { + throw new \RuntimeException(sprintf('Duplicate source: %s (ID already used by %s)', $id, $sources[$id]::class)); + } + $sources[$id] = $source; + } + $this->indexed = $sources; + } + + return $this->indexed; + } + + public function getSource(string $name): SourceInterface + { + $sources = $this->getSources(); + if (!array_key_exists($name, $sources)) { + throw new \InvalidArgumentException(sprintf('Source "%s" does not exist.', $name)); + } + + return $sources[$name]; + } +} diff --git a/src/ValueResolver/SourceArgumentValueResolver.php b/src/ValueResolver/SourceArgumentValueResolver.php new file mode 100644 index 0000000..756ab33 --- /dev/null +++ b/src/ValueResolver/SourceArgumentValueResolver.php @@ -0,0 +1,34 @@ + + */ + public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable + { + /** @var \ReflectionNamedType $type */ + $type = $member->getType(); + if (SourceInterface::class !== $type->getName()) { + return []; + } + $value = $input->getArgument($argumentName); + + return [ + $this->sourceManager->getSource($value), + ]; + } +} diff --git a/tests/Command/ImportCommandTest.php b/tests/Command/ImportCommandTest.php index 6d4c7ca..828e17f 100644 --- a/tests/Command/ImportCommandTest.php +++ b/tests/Command/ImportCommandTest.php @@ -33,7 +33,7 @@ private function tester(iterable $sources, ?MockHttpClient $client = null): Comm { $keys = []; foreach ($sources as $source) { - $keys[] = $source->key(); + $keys[] = $source->id; } return new CommandTester(new ImportCommand(new DataSourceImporter( diff --git a/tests/Import/DataSourceImporterTest.php b/tests/Import/DataSourceImporterTest.php index 3e838a7..6686577 100644 --- a/tests/Import/DataSourceImporterTest.php +++ b/tests/Import/DataSourceImporterTest.php @@ -35,7 +35,7 @@ private function importer( ): DataSourceImporter { $keys = []; foreach ($sources as $source) { - $keys[] = $source->key(); + $keys[] = $source->id; } return new DataSourceImporter( From 4fb6dabf00851a3cb5f86dee58312eeb01b3cfdc Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 9 Sep 2026 12:04:39 +0200 Subject: [PATCH 061/101] Added scorpio to development setup. Removed database. --- README.md | 9 ++++++++- docker-compose.override.yml | 9 +++++++++ docker-compose.scorpio.yml | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.scorpio.yml diff --git a/README.md b/README.md index c80ab34..1ea72f0 100644 --- a/README.md +++ b/README.md @@ -51,5 +51,12 @@ Design decisions are recorded in [docs/adr](docs/adr/README.md). A [Scorpio Broker](https://scorpio.readthedocs.io/) is part of the development setup. ``` shell -ddev exec "curl --silent http://scorpio.local:9090/ngsi-ld/v1/types | jq" +docker compose exec phpfpm curl --silent http://scorpio.local:9090/ngsi-ld/v1/types | jq +``` + +If you're using the [ITK-dev docker setup](https://github.com/itk-dev/devops_itkdev-docker/), the broker can also be +accessed on , e.g. + +``` php +curl --silent http://scorpio.enter.local.itkdev.dk/ngsi-ld/v1/types | jq ``` diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..0595c01 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,9 @@ +include: + - docker-compose.scorpio.yml + +services: + # We don't (yet?) need a database. + mariadb: !reset + + phpfpm: + depends_on: !reset diff --git a/docker-compose.scorpio.yml b/docker-compose.scorpio.yml new file mode 100644 index 0000000..c12278e --- /dev/null +++ b/docker-compose.scorpio.yml @@ -0,0 +1,37 @@ +networks: + scorpio: + +# https://docs.ddev.com/en/stable/users/extend/custom-docker-services/ +services: + scorpio: + image: scorpiobroker/all-in-one-runner:java-latest + restart: "no" + ports: + - "9090" + environment: + - HTTP_EXPOSE=9090:9999 + - DBHOST=scorpio-db + depends_on: + - "scorpio-db" + networks: + frontend: + app: + aliases: [scorpio.local] + scorpio: + platform: "linux/amd64" + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.scorpio_${COMPOSE_PROJECT_NAME:?}.rule=Host(`scorpio.${COMPOSE_DOMAIN:?}`)" + + scorpio-db: + image: postgis/postgis + environment: + - POSTGRES_USER=ngb + - POSTGRES_PASSWORD=ngb + - POSTGRES_DB=ngb + networks: + - scorpio + platform: "linux/amd64" + # https://stackoverflow.com/a/58806511 + command: ["postgres", "-c", "log_statement=all"] From 56ef3f077d7f6bbbcc3fc541d21a457e4e16d967 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 9 Sep 2026 12:49:07 +0200 Subject: [PATCH 062/101] Updated docker compose setup --- README.md | 18 +++-- Taskfile.yml | 103 ++++++++++++++++++++++++----- composer.json | 10 +-- composer.lock | 88 ++++++++++++------------ docker-compose.override.yml | 1 + docker-compose.scorpio.yml | 1 - docker-compose.server.override.yml | 19 +++--- 7 files changed, 160 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 1ea72f0..bf3dc66 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,25 @@ # Enter -We use [DDEV](https://ddev.com/) and [Task](https://taskfile.dev/) for development: +We use [ITK-dev docker setup] or [DDEV](https://ddev.com/) and [Task](https://taskfile.dev/) for development: ``` shell task site:install ``` +Set + +``` dotenv +TASK_USE_DDEV=true +``` + +in `.env.local` to make `task` use `ddev` rather than `docker compose` for running commands. + ``` shell task site:update -ddev launch +task site:open ``` -Run `task` to see what cool task are available. Running `ddev` can help with other stuff. +Run `task` to see what cool task are available. ## Adapter @@ -54,9 +62,11 @@ A [Scorpio Broker](https://scorpio.readthedocs.io/) is part of the development s docker compose exec phpfpm curl --silent http://scorpio.local:9090/ngsi-ld/v1/types | jq ``` -If you're using the [ITK-dev docker setup](https://github.com/itk-dev/devops_itkdev-docker/), the broker can also be +If you're using the [ITK-dev docker setup], the broker can also be accessed on , e.g. ``` php curl --silent http://scorpio.enter.local.itkdev.dk/ngsi-ld/v1/types | jq ``` + +[ITK-dev docker setup]: https://github.com/itk-dev/devops_itkdev-docker/ diff --git a/Taskfile.yml b/Taskfile.yml index 84fbd88..a1b1178 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -8,17 +8,36 @@ tasks: desc: Install site from scratch prompt: Really (re)install site? cmds: - - ddev delete --omit-snapshot --yes - - rm -fr .ddev/data/scorpio-db/ + - if: '[ "$TASK_USE_DDEV" = "true" ]' + cmd: | + ddev delete --omit-snapshot --yes + rm -fr .ddev/data/scorpio-db/ - task: site:update site:update: desc: Update site cmds: - - ddev restart - - ddev composer install - # - ddev console doctrine:migrations:migrate --no-interaction - - ddev console cache:clear + - if: '[ "$TASK_USE_DDEV" != "true" ]' + task: compose + vars: + TASK_ARGS: pull + - if: '[ "$TASK_USE_DDEV" = "true" ]' + cmd: ddev restart + - task: composer:install + # - task: console + # vars: + # TASK_ARGS: doctrine:migrations:migrate --no-interaction + - task: console + vars: + TASK_ARGS: cache:clear + + site:open: + desc: Open the site in a browser + cmds: + - if: '[ "$TASK_USE_DDEV" != "true" ]' + cmd: open 'http://enter.local.itkdev.dk/' + - if: '[ "$TASK_USE_DDEV" = "true" ]' + cmd: ddev launch test: desc: Run all tests @@ -31,14 +50,20 @@ tasks: env: APP_ENV: test desc: 'Run all {{.TASK | replace "test:" "" }} tests' - cmd: ddev php bin/phpunit tests/{{.TASK | replace "test:" "" | title }} + cmd: + task: php + vars: + TASK_ARGS: bin/phpunit tests/{{.TASK | replace "test:" "" | title }} test:integration: *test_task test:application: *test_task import: - desc: 'Import a source into the broker, e.g. task import -- mtm_spatialmaps-handicap-parking' - cmd: ddev console app:import {{.CLI_ARGS}} + desc: 'Import a source into the broker, e.g. `task {{.TASK}} -- mtm_spatialmaps-handicap-parking`' + cmd: + task: console + vars: + TASK_ARGS: app:import coding-standards:apply: desc: 'Apply coding standards' @@ -114,26 +139,38 @@ tasks: coding-standards:php:apply: desc: 'Apply coding standards for PHP' cmds: - - ddev php vendor/bin/rector - - ddev php vendor/bin/php-cs-fixer fix + - task: php + vars: + TASK_ARGS: vendor/bin/rector + - task: php + vars: + TASK_ARGS: vendor/bin/php-cs-fixer fix coding-standards:php:check: desc: 'Apply and check coding standards for PHP' cmds: - task: coding-standards:php:apply - - ddev php vendor/bin/rector --dry-run - - ddev php vendor/bin/php-cs-fixer check + - task: php + vars: + TASK_ARGS: vendor/bin/rector --dry-run + - task: php + vars: + TASK_ARGS: vendor/bin/php-cs-fixer check coding-standards:twig:apply: desc: 'Apply coding standards for Twig' cmds: - - ddev php vendor/bin/twig-cs-fixer fix + - task: php + vars: + TASK_ARGS: vendor/bin/twig-cs-fixer fix coding-standards:twig:check: desc: 'Apply and check coding standards for Twig' cmds: - task: coding-standards:twig:apply - - ddev php vendor/bin/twig-cs-fixer check + - task: php + vars: + TASK_ARGS: vendor/bin/twig-cs-fixer check coding-standards:yaml:apply: desc: 'Apply coding standards for YAML' @@ -172,9 +209,39 @@ tasks: composer: internal: true - task: compose - vars: - TASK_ARGS: run --rm --no-deps phpfpm composer + cmd: + task: compose + vars: + TASK_ARGS: run --rm --no-deps phpfpm composer {{.TASK_ARGS}} + + composer:install: + internal: true + cmds: + - if: '[ "$APP_ENV" != "prod" ]' + task: composer + vars: + TASK_ARGS: install + - if: '[ "$APP_ENV" = "prod" ]' + task: composer + vars: + TASK_ARGS: install --no-dev --classmap-authoritative + + console: + internal: true + cmd: + task: php + vars: + TASK_ARGS: bin/console {{.TASK_ARGS}} + + php: + internal: true + cmds: + - if: '[ "$TASK_USE_DDEV" != "true" ]' + task: compose + vars: + TASK_ARGS: exec phpfpm php {{.TASK_ARGS}} + - if: '[ "$TASK_USE_DDEV" = "true" ]' + cmd: ddev php {{.TASK_ARGS}} default: cmds: diff --git a/composer.json b/composer.json index fad8d55..95ff344 100644 --- a/composer.json +++ b/composer.json @@ -30,17 +30,17 @@ }, "require-dev": { "ergebnis/composer-normalize": "^2.52", - "friendsofphp/php-cs-fixer": "^3.95.24", - "phpstan/phpstan": "^2.2.12", - "phpunit/phpunit": "^13.3", - "rector/rector": "^2.6", + "friendsofphp/php-cs-fixer": "^3.95.25", + "phpstan/phpstan": "^2.2.13", + "phpunit/phpunit": "^13.3.3", + "rector/rector": "^2.6.6", "symfony/browser-kit": "~8.1.0", "symfony/css-selector": "~8.1.6", "symfony/debug-bundle": "~8.1.0", "symfony/maker-bundle": "^1.67", "symfony/stopwatch": "~8.1.0", "symfony/web-profiler-bundle": "~8.1.0", - "vincentlanglet/twig-cs-fixer": "^4.1" + "vincentlanglet/twig-cs-fixer": "^4.1.1" }, "replace": { "symfony/polyfill-ctype": "*", diff --git a/composer.lock b/composer.lock index 2d8ddb4..5eceb64 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dfaceee7643a3761619ee1e8ae943e6a", + "content-hash": "15f5017ca0bc1b5d638704a1f9d9a303", "packages": [ { "name": "composer/semver", @@ -5165,16 +5165,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.95.24", + "version": "v3.95.25", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "9a4b8050f8768646bf3693ee7416978ceeca6206" + "reference": "2cdfc1f3daf173d83a1ebb6177949b58c95de6ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9a4b8050f8768646bf3693ee7416978ceeca6206", - "reference": "9a4b8050f8768646bf3693ee7416978ceeca6206", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2cdfc1f3daf173d83a1ebb6177949b58c95de6ff", + "reference": "2cdfc1f3daf173d83a1ebb6177949b58c95de6ff", "shasum": "" }, "require": { @@ -5257,7 +5257,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.24" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.25" }, "funding": [ { @@ -5265,20 +5265,20 @@ "type": "github" } ], - "time": "2026-08-31T15:31:12+00:00" + "time": "2026-09-08T11:11:37+00:00" }, { "name": "justinrainbow/json-schema", - "version": "6.11.0", + "version": "6.12.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7" + "reference": "8ef236a1a37df364b518b7411a4b5bb95b040d79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/7e420a943a6fbc95e60e3cf67acfbee85b3b4da7", - "reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8ef236a1a37df364b518b7411a4b5bb95b040d79", + "reference": "8ef236a1a37df364b518b7411a4b5bb95b040d79", "shasum": "" }, "require": { @@ -5328,9 +5328,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.11.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.12.0" }, - "time": "2026-08-21T10:30:42+00:00" + "time": "2026-09-04T12:54:20+00:00" }, { "name": "localheinz/diff", @@ -5697,11 +5697,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.12", + "version": "2.2.13", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/174b0d88710f00a42598886504dd7a146f91ace5", - "reference": "174b0d88710f00a42598886504dd7a146f91ace5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9ba9ac76ee9c5cf5b56d58eb5deec6315b7a0260", + "reference": "9ba9ac76ee9c5cf5b56d58eb5deec6315b7a0260", "shasum": "" }, "require": { @@ -5757,20 +5757,20 @@ "type": "github" } ], - "time": "2026-08-31T19:09:43+00:00" + "time": "2026-09-03T20:38:19+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "14.3.1", + "version": "14.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6ce313bb110384148d1dc7695a99175f59529069" + "reference": "93d62632fb675b76c9b941fdcfcfa10ffb9dea75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6ce313bb110384148d1dc7695a99175f59529069", - "reference": "6ce313bb110384148d1dc7695a99175f59529069", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/93d62632fb675b76c9b941fdcfcfa10ffb9dea75", + "reference": "93d62632fb675b76c9b941fdcfcfa10ffb9dea75", "shasum": "" }, "require": { @@ -5789,7 +5789,7 @@ "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^13.3.1" + "phpunit/phpunit": "^13.3.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -5827,7 +5827,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.3.1" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.3.2" }, "funding": [ { @@ -5847,7 +5847,7 @@ "type": "tidelift" } ], - "time": "2026-08-16T05:23:47+00:00" + "time": "2026-09-04T03:46:43+00:00" }, { "name": "phpunit/php-file-iterator", @@ -6144,16 +6144,16 @@ }, { "name": "phpunit/phpunit", - "version": "13.3.2", + "version": "13.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "22104a5ceb8d642e6b30ab00211d53d88e2db368" + "reference": "0d8711067516c1f6ba1a66f762e16c2fb1a8ccd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/22104a5ceb8d642e6b30ab00211d53d88e2db368", - "reference": "22104a5ceb8d642e6b30ab00211d53d88e2db368", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0d8711067516c1f6ba1a66f762e16c2fb1a8ccd1", + "reference": "0d8711067516c1f6ba1a66f762e16c2fb1a8ccd1", "shasum": "" }, "require": { @@ -6167,14 +6167,14 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.4.1", - "phpunit/php-code-coverage": "^14.3.1", + "phpunit/php-code-coverage": "^14.3.2", "phpunit/php-file-iterator": "^7.0.2", "phpunit/php-invoker": "^7.0.0", "phpunit/php-text-template": "^6.0.0", "phpunit/php-timer": "^9.0.0", "sebastian/cli-parser": "^5.0.1", "sebastian/comparator": "^8.4", - "sebastian/diff": "^9.0", + "sebastian/diff": "^9.0.1", "sebastian/environment": "^9.3.2", "sebastian/exporter": "^8.2.1", "sebastian/file-filter": "^1.0", @@ -6224,7 +6224,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/13.3.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.3.3" }, "funding": [ { @@ -6232,7 +6232,7 @@ "type": "other" } ], - "time": "2026-08-27T08:40:49+00:00" + "time": "2026-09-09T04:50:32+00:00" }, { "name": "react/cache", @@ -6762,16 +6762,16 @@ }, { "name": "rector/rector", - "version": "2.6.5", + "version": "2.6.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "f8686605798c755456327a3597201c3b5be76294" + "reference": "ca069d6c79feaa6651b423c15d101a27a436093e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/f8686605798c755456327a3597201c3b5be76294", - "reference": "f8686605798c755456327a3597201c3b5be76294", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/ca069d6c79feaa6651b423c15d101a27a436093e", + "reference": "ca069d6c79feaa6651b423c15d101a27a436093e", "shasum": "" }, "require": { @@ -6807,7 +6807,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.6.5" + "source": "https://github.com/rectorphp/rector/tree/2.6.6" }, "funding": [ { @@ -6815,7 +6815,7 @@ "type": "github" } ], - "time": "2026-08-30T22:02:04+00:00" + "time": "2026-09-02T09:38:46+00:00" }, { "name": "sebastian/cli-parser", @@ -8699,16 +8699,16 @@ }, { "name": "vincentlanglet/twig-cs-fixer", - "version": "4.1.0", + "version": "4.1.1", "source": { "type": "git", "url": "https://github.com/VincentLanglet/Twig-CS-Fixer.git", - "reference": "73f6474720fce9a6709e7b6cdd26574efe36d09b" + "reference": "53c1234b6e5d87ac2beaf686774bba013697f9c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/73f6474720fce9a6709e7b6cdd26574efe36d09b", - "reference": "73f6474720fce9a6709e7b6cdd26574efe36d09b", + "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/53c1234b6e5d87ac2beaf686774bba013697f9c5", + "reference": "53c1234b6e5d87ac2beaf686774bba013697f9c5", "shasum": "" }, "require": { @@ -8764,7 +8764,7 @@ "homepage": "https://github.com/VincentLanglet/Twig-CS-Fixer", "support": { "issues": "https://github.com/VincentLanglet/Twig-CS-Fixer/issues", - "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/4.1.0" + "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/4.1.1" }, "funding": [ { @@ -8772,7 +8772,7 @@ "type": "github" } ], - "time": "2026-08-30T16:07:04+00:00" + "time": "2026-09-08T21:26:25+00:00" } ], "aliases": [], diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 0595c01..d63ff9a 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -6,4 +6,5 @@ services: mariadb: !reset phpfpm: + image: itkdev/php8.5-fpm:latest depends_on: !reset diff --git a/docker-compose.scorpio.yml b/docker-compose.scorpio.yml index c12278e..2a5bb02 100644 --- a/docker-compose.scorpio.yml +++ b/docker-compose.scorpio.yml @@ -1,7 +1,6 @@ networks: scorpio: -# https://docs.ddev.com/en/stable/users/extend/custom-docker-services/ services: scorpio: image: scorpiobroker/all-in-one-runner:java-latest diff --git a/docker-compose.server.override.yml b/docker-compose.server.override.yml index 9a3a124..3eabfdd 100644 --- a/docker-compose.server.override.yml +++ b/docker-compose.server.override.yml @@ -1,15 +1,13 @@ networks: scorpio: -# https://docs.ddev.com/en/stable/users/extend/custom-docker-services/ services: phpfpm: image: itkdev/php8.5-fpm:alpine scorpio: - hostname: scorpio.local image: scorpiobroker/all-in-one-runner:java-latest - restart: "no" + restart: unless-stopped ports: - "9090" environment: @@ -17,20 +15,25 @@ services: depends_on: - "scorpio-db" networks: - - app - - scorpio + app: + aliases: [scorpio.local] + scorpio: platform: "linux/amd64" scorpio-db: image: postgis/postgis + restart: unless-stopped environment: - POSTGRES_USER=ngb - POSTGRES_PASSWORD=ngb - POSTGRES_DB=ngb + healthcheck: + test: ["CMD-SHELL", "pg_isready --username=ngb --dbname=ngb"] + start_period: 10s + interval: 5s + timeout: 5s + retries: 10 networks: - scorpio volumes: - ./.docker/data/scorpio-db:/var/lib/postgresql/data - platform: "linux/amd64" - # https://stackoverflow.com/a/58806511 - command: ["postgres", "-c", "log_statement=all"] From 5ad6f0a76769549dfdd1add9277a49675d4a2b69 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 9 Sep 2026 14:03:03 +0200 Subject: [PATCH 063/101] Added Monolog --- composer.json | 1 + composer.lock | 264 +++++++++++++++++- config/bundles.php | 1 + config/packages/monolog.yaml | 55 ++++ config/reference.php | 149 ++++++++++ ...eadCommand.php => SourceImportCommand.php} | 0 symfony.lock | 12 + 7 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 config/packages/monolog.yaml rename src/Command/{SourceReadCommand.php => SourceImportCommand.php} (100%) diff --git a/composer.json b/composer.json index 95ff344..1c0b306 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "symfony/flex": "^2", "symfony/framework-bundle": "~8.1.6", "symfony/http-client": "~8.1.6", + "symfony/monolog-bundle": "^4.0", "symfony/property-access": "~8.1.0", "symfony/property-info": "~8.1.6", "symfony/runtime": "~8.1.0", diff --git a/composer.lock b/composer.lock index 5eceb64..f72a09c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "15f5017ca0bc1b5d638704a1f9d9a303", + "content-hash": "21d5558271e5ac022aa9b190bf555602", "packages": [ { "name": "composer/semver", @@ -178,6 +178,111 @@ }, "time": "2026-04-02T10:02:52+00:00" }, + { + "name": "monolog/monolog", + "version": "3.12.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "72c534fc0ab181ef52d92a68382318631e301608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/72c534fc0ab181ef52d92a68382318631e301608", + "reference": "72c534fc0ab181ef52d92a68382318631e301608", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.12.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-09-09T08:34:20+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -2374,6 +2479,163 @@ ], "time": "2026-08-30T21:40:49+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v8.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "90747e82447b92c0fc701bfaf99bac0d54127039" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/90747e82447b92c0fc701bfaf99bac0d54127039", + "reference": "90747e82447b92c0fc701bfaf99bac0d54127039", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.4.1", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/mailer": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/notifier": "^7.4|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v8.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T01:03:44+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "c012c6aba13129eb02aa7dd61e66e720911d8598" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/c012c6aba13129eb02aa7dd61e66e720911d8598", + "reference": "c012c6aba13129eb02aa7dd61e66e720911d8598", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "monolog/monolog": "^3.5", + "php": ">=8.2", + "symfony/config": "^7.3 || ^8.0", + "symfony/dependency-injection": "^7.3 || ^8.0", + "symfony/http-kernel": "^7.3 || ^8.0", + "symfony/monolog-bridge": "^7.3 || ^8.0", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.41 || ^12.3", + "symfony/console": "^7.3 || ^8.0", + "symfony/yaml": "^7.3 || ^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v4.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-02T18:27:21+00:00" + }, { "name": "symfony/polyfill-deepclone", "version": "v1.42.0", diff --git a/config/bundles.php b/config/bundles.php index 93280e5..5b067eb 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -7,4 +7,5 @@ Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], ]; diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml new file mode 100644 index 0000000..e09985f --- /dev/null +++ b/config/packages/monolog.yaml @@ -0,0 +1,55 @@ +monolog: + channels: # To see all channels, run bin/console debug:autowiring logger + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: '%kernel.logs_dir%/%kernel.environment%.log' + level: debug + channels: ['!event'] + console: + type: console + process_psr_3_messages: false + channels: ['!event', '!doctrine', '!console'] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ['!event'] + nested: + type: stream + path: '%kernel.logs_dir%/%kernel.environment%.log' + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ['!deprecation'] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ['!event', '!doctrine'] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr + formatter: monolog.formatter.json diff --git a/config/reference.php b/config/reference.php index 1af6c7d..aea1b6f 100644 --- a/config/reference.php +++ b/config/reference.php @@ -801,6 +801,151 @@ * dump_destination?: scalar|Param|null, // A stream URL where dumps should be written to. // Default: null * theme?: "dark"|"light"|Param, // Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light". // Default: "dark" * } + * @psalm-type MonologConfig = array{ + * use_microseconds?: scalar|Param|null, // Default: true + * channels?: list, + * handlers?: array + * }, + * path?: scalar|Param|null, // Default: "%kernel.logs_dir%/%kernel.environment%.log" + * file_permission?: scalar|Param|null, // Default: null + * use_locking?: bool|Param, // Default: false + * filename_format?: scalar|Param|null, // Default: "{filename}-{date}" + * date_format?: scalar|Param|null, // Default: "Y-m-d" + * ident?: scalar|Param|null, // Default: false + * logopts?: scalar|Param|null, // Default: 1 + * facility?: scalar|Param|null, // Default: "user" + * max_files?: scalar|Param|null, // Default: 0 + * action_level?: scalar|Param|null, // Default: "WARNING" + * activation_strategy?: scalar|Param|null, // Default: null + * stop_buffering?: bool|Param, // Default: true + * passthru_level?: scalar|Param|null, // Default: null + * excluded_http_codes?: list, + * }>, + * accepted_levels?: list, + * min_level?: scalar|Param|null, // Default: "DEBUG" + * max_level?: scalar|Param|null, // Default: "EMERGENCY" + * buffer_size?: scalar|Param|null, // Default: 0 + * flush_on_overflow?: bool|Param, // Default: false + * handler?: scalar|Param|null, + * url?: scalar|Param|null, + * exchange?: scalar|Param|null, + * exchange_name?: scalar|Param|null, // Default: "log" + * channel?: scalar|Param|null, // Default: null + * bot_name?: scalar|Param|null, // Default: "Monolog" + * use_attachment?: scalar|Param|null, // Default: true + * use_short_attachment?: scalar|Param|null, // Default: false + * include_extra?: scalar|Param|null, // Default: false + * icon_emoji?: scalar|Param|null, // Default: null + * webhook_url?: scalar|Param|null, + * exclude_fields?: list, + * token?: scalar|Param|null, + * region?: scalar|Param|null, + * source?: scalar|Param|null, + * use_ssl?: bool|Param, // Default: true + * user?: mixed, + * title?: scalar|Param|null, // Default: null + * host?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 514 + * config?: list, + * members?: list, + * connection_string?: scalar|Param|null, + * timeout?: scalar|Param|null, + * time?: scalar|Param|null, // Default: 60 + * deduplication_level?: scalar|Param|null, // Default: 400 + * store?: scalar|Param|null, // Default: null + * connection_timeout?: scalar|Param|null, + * persistent?: bool|Param, + * message_type?: scalar|Param|null, // Default: 0 + * parse_mode?: scalar|Param|null, // Default: null + * disable_webpage_preview?: bool|Param|null, // Default: null + * disable_notification?: bool|Param|null, // Default: null + * split_long_messages?: bool|Param, // Default: false + * delay_between_messages?: bool|Param, // Default: false + * topic?: int|Param, // Default: null + * factor?: int|Param, // Default: 1 + * tags?: Param|string|list, + * console_formatter_options?: mixed, // Default: [] + * formatter?: scalar|Param|null, + * nested?: bool|Param, // Default: false + * publisher?: Param|string|array{ + * id?: scalar|Param|null, + * hostname?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 12201 + * chunk_size?: scalar|Param|null, // Default: 1420 + * encoder?: "json"|"compressed_json"|Param, + * }, + * mongodb?: Param|string|array{ + * id?: scalar|Param|null, // ID of a MongoDB\Client service + * uri?: scalar|Param|null, + * username?: scalar|Param|null, + * password?: scalar|Param|null, + * database?: scalar|Param|null, // Default: "monolog" + * collection?: scalar|Param|null, // Default: "logs" + * }, + * elasticsearch?: Param|string|array{ + * id?: scalar|Param|null, + * hosts?: list, + * host?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 9200 + * transport?: scalar|Param|null, // Default: "Http" + * user?: scalar|Param|null, // Default: null + * password?: scalar|Param|null, // Default: null + * }, + * index?: scalar|Param|null, // Default: "monolog" + * document_type?: scalar|Param|null, // Default: "logs" + * ignore_error?: scalar|Param|null, // Default: false + * redis?: Param|string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * password?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 6379 + * database?: scalar|Param|null, // Default: 0 + * key_name?: scalar|Param|null, // Default: "monolog_redis" + * }, + * predis?: Param|string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * }, + * from_email?: scalar|Param|null, + * to_email?: Param|string|list, + * subject?: scalar|Param|null, + * content_type?: scalar|Param|null, // Default: null + * headers?: list, + * mailer?: scalar|Param|null, // Default: null + * email_prototype?: Param|string|array{ + * id?: scalar|Param|null, + * method?: scalar|Param|null, // Default: null + * }, + * verbosity_levels?: array{ + * VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR" + * VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING" + * VERBOSITY_VERBOSE?: scalar|Param|null, // Default: "NOTICE" + * VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: "INFO" + * VERBOSITY_DEBUG?: scalar|Param|null, // Default: "DEBUG" + * }, + * channels?: Param|string|array{ + * type?: scalar|Param|null, + * elements?: list, + * ... + * }, + * }>, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -808,6 +953,7 @@ * framework?: FrameworkConfig, * twig?: TwigConfig, * twig_extra?: TwigExtraConfig, + * monolog?: MonologConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -818,6 +964,7 @@ * twig_extra?: TwigExtraConfig, * web_profiler?: WebProfilerConfig, * debug?: DebugConfig, + * monolog?: MonologConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -826,6 +973,7 @@ * framework?: FrameworkConfig, * twig?: TwigConfig, * twig_extra?: TwigExtraConfig, + * monolog?: MonologConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -835,6 +983,7 @@ * twig?: TwigConfig, * twig_extra?: TwigExtraConfig, * web_profiler?: WebProfilerConfig, + * monolog?: MonologConfig, * }, * ... Date: Wed, 9 Sep 2026 14:09:28 +0200 Subject: [PATCH 064/101] Refactored source import stuff --- .env | 10 +- src/Command/SourceImportCommand.php | 22 +++-- src/Ngsi/NgsiEntity.php | 2 +- src/Source/MtmSpatialMaps/HandicapParking.php | 37 ++----- src/Source/SourceInterface.php | 5 +- src/SourceImporterFactory.php | 35 +++++++ src/SourceImporterGeoJson.php | 97 +++++++++++++++++++ src/SourceImporterInterface.php | 18 ++++ src/SourceReaderGeoJson.php | 49 ++++++++++ src/SourceReaderInterface.php | 13 +++ .../MtmSpatialMaps/HandicapParkingTest.php | 2 +- 11 files changed, 246 insertions(+), 44 deletions(-) create mode 100644 src/SourceImporterFactory.php create mode 100644 src/SourceImporterGeoJson.php create mode 100644 src/SourceImporterInterface.php create mode 100644 src/SourceReaderGeoJson.php create mode 100644 src/SourceReaderInterface.php diff --git a/.env b/.env index 819e914..45fd128 100644 --- a/.env +++ b/.env @@ -33,8 +33,10 @@ DEFAULT_URI=http://localhost ###> app ### APP_BROKER_BASE_URI=http://scorpio.local:9090/ -# JSON-LD contexts attached to every entity, outermost last so the ETSI core -# context resolves the NGSI-LD terms and the domain context resolves the -# Smart Data Models ones. -ENTER_NGSI_CONTEXT_URLS='https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld' +# A (JSON) list of JSON-LD contexts attached to every entity, outermost last so the +# ETSI core context resolves the NGSI-LD terms and the domain context resolves +# the Smart Data Models ones. +APP_NGSI_CONTEXT_URLS='[ +"https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" +]' ###< app ### diff --git a/src/Command/SourceImportCommand.php b/src/Command/SourceImportCommand.php index cbbaba6..2ab733c 100644 --- a/src/Command/SourceImportCommand.php +++ b/src/Command/SourceImportCommand.php @@ -4,23 +4,33 @@ use App\Source\DataSourceReader; use App\Source\SourceInterface; +use App\SourceImporterFactory; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( - name: 'app:source:read', + name: 'app:source:import', )] -class SourceReadCommand +class SourceImportCommand { public function __invoke(SymfonyStyle $io, DataSourceReader $reader, + SourceImporterFactory $factory, #[Argument] SourceInterface $source): int { - throw new \RuntimeException('Lazy programmer exception!'); - // $reader = $readerFactory->getReader($source); - // $reader->read($source); - // … + $importer = $factory->getSourceImporter($source); + $result = $importer->import($source); + + $io->success(sprintf( + 'Upserted %d entities into %s (HTTP %d).', + $result->count, + $result->brokerUrl, + $result->status + )); + + return Command::SUCCESS; } } diff --git a/src/Ngsi/NgsiEntity.php b/src/Ngsi/NgsiEntity.php index dbc2f79..97d781a 100644 --- a/src/Ngsi/NgsiEntity.php +++ b/src/Ngsi/NgsiEntity.php @@ -69,7 +69,7 @@ public function relationship(string $name, string $object): self * * @return array */ - public function toArray(array $contextUrls): array + public function toPayload(array $contextUrls): array { return [ 'id' => $this->id, diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 3ea503c..1ccf26a 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -7,21 +7,14 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; -use App\Source\DataSourceReader; -use App\Source\Manifest\Catalog; -use App\Source\Manifest\Descriptor; /** * Disabled parking bays in Aarhus Municipality. */ final readonly class HandicapParking extends AbstractSource { - private const string KEY = 'mtm_spatialmaps-handicap-parking'; - public function __construct( - private DataSourceReader $reader, private Wgs84Transformer $transformer, - private Catalog $catalog, ) { parent::__construct( id: 'mtm_spatialmaps-handicap-parking', @@ -48,29 +41,15 @@ public function __construct( ); } - public function entities(): iterable - { - $source = $this->catalog->get(self::KEY); - - // The export is a GeoJSON FeatureCollection, so the records live under - // `features`. Iterating the document itself would walk its two - // top-level keys instead. - foreach ($this->reader->read($source->accessUrl)['features'] ?? [] as $feature) { - if (\is_array($feature) && null !== $entity = $this->toEntity($feature, $source)) { - yield $entity; - } - } - } - /** * Maps one feed record onto an NgsiEntity. * - * @param array $feature GeoJSON Feature + * @param array $data GeoJSON Feature */ - private function toEntity(array $feature, Descriptor $source): ?NgsiEntity + public function createNgsiEntity(array $data): ?NgsiEntity { - $row = $feature['properties'] ?? null; - $geometry = $feature['geometry'] ?? null; + $row = $data['properties'] ?? null; + $geometry = $data['geometry'] ?? null; if (!\is_array($row) || !\is_array($geometry)) { return null; @@ -85,8 +64,8 @@ private function toEntity(array $feature, Descriptor $source): ?NgsiEntity } $entity = new NgsiEntity( - \sprintf('urn:ngsi-ld:%s:aarhus-handicap-%s', $source->model, $key), - $source->model + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-%s', $this->model, $key), + $this->model ); return $entity @@ -94,8 +73,8 @@ private function toEntity(array $feature, Descriptor $source): ?NgsiEntity ->setProperty('description', trim((string) ($row['bemrk'] ?? ''))) ->setProperty('category', ['forDisabled']) ->setProperty('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) - ->setProperty('source', $source->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($source->crs, $geometry)); + ->setProperty('source', $this->accessUrl) + ->geoProperty('location', $this->transformer->transformGeometry($this->crs, $geometry)); } /** diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php index 1d6d68d..e010081 100644 --- a/src/Source/SourceInterface.php +++ b/src/Source/SourceInterface.php @@ -78,10 +78,9 @@ interface SourceInterface extends \Stringable, \JsonSerializable } /** - * @return iterable + * @param array $data */ - // We should let the (data) source reader read. - public function entities(): iterable; + public function createNgsiEntity(array $data): ?NgsiEntity; /** * @return array diff --git a/src/SourceImporterFactory.php b/src/SourceImporterFactory.php new file mode 100644 index 0000000..aee58ea --- /dev/null +++ b/src/SourceImporterFactory.php @@ -0,0 +1,35 @@ + $importers + */ + public function __construct( + #[AutowireIterator('app.source_importer')] + private iterable $importers, + ) { + } + + public function getSourceImporter(SourceInterface $source): SourceImporterInterface + { + foreach ($this->importers as $importer) { + if ($importer->supports($source)) { + return $importer; + } + } + + // @todo + throw new \RuntimeException('Cannot get importer for source'); + } +} diff --git a/src/SourceImporterGeoJson.php b/src/SourceImporterGeoJson.php new file mode 100644 index 0000000..8acd977 --- /dev/null +++ b/src/SourceImporterGeoJson.php @@ -0,0 +1,97 @@ + $contextUrls + */ + public function __construct( + private readonly SourceReaderGeoJson $reader, + private readonly NgsiLdBroker $broker, + #[Autowire(env: 'json:APP_NGSI_CONTEXT_URLS')] + private readonly array $contextUrls, + LoggerInterface $logger, + ) { + $this->setLogger($logger); + } + + public function supports(SourceInterface $source): bool + { + // @todo + return true; + } + + public function import(SourceInterface $source): ImportResult + { + $this->info('Processing source {source}', ['source' => $source->__toString()]); + + $contextUrls = array_merge([$source->contextUrl], $this->contextUrls); + $payload = []; + foreach ($this->read($source) as $entity) { + $this->info('Building payload for {entity}', ['entity' => $entity->id()]); + $payload[] = $entity->toPayload($contextUrls); + } + + if (1 === count($payload)) { + $this->info('Upserting 1 entity'); + } else { + $this->info('Upserting {count} entities', ['count' => count($payload)]); + } + + try { + $status = $this->broker->upsert($payload); + } catch (\Throwable $exception) { + throw new UpsertFailedException($exception); + } + + return new ImportResult(\count($payload), $status, $this->broker->brokerUrl()); + } + + /** + * @return iterable + */ + private function read(SourceInterface $source): iterable + { + $data = $this->reader->read($source); + foreach ($data as $item) { + if (is_array($item)) { + try { + if ($entity = $source->createNgsiEntity($item)) { + yield $entity; + } + } catch (\Exception $e) { + // @todo Log in database? + $this->error('error: {message} ', ['message' => $e->getMessage()]); + } + } + } + } + + /** + * @param array $context + */ + public function log($level, \Stringable|string $message, array $context = []): void + { + $this->logger->log($level, $message, $context); + } +} diff --git a/src/SourceImporterInterface.php b/src/SourceImporterInterface.php new file mode 100644 index 0000000..65391fd --- /dev/null +++ b/src/SourceImporterInterface.php @@ -0,0 +1,18 @@ +getData($source->accessUrl); + + $features = $data['features']; + + if (!is_array($features) || !array_is_list($features)) { + throw new \RuntimeException('Invalid features array'); + } + + return $features; + } + + /** + * @return array + */ + private function getData(string $url): iterable + { + return $this->client->request('GET', $url)->toArray(); + } + + /** + * @param array $context + */ + public function log($level, \Stringable|string $message, array $context = []): void + { + $this->logger->log($level, $message, $context); + } +} diff --git a/src/SourceReaderInterface.php b/src/SourceReaderInterface.php new file mode 100644 index 0000000..311f398 --- /dev/null +++ b/src/SourceReaderInterface.php @@ -0,0 +1,13 @@ + + */ + public function read(SourceInterface $source): iterable; +} diff --git a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php index 6517824..ff6b8f9 100644 --- a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php +++ b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php @@ -43,7 +43,7 @@ protected function setUp(): void $source = new HandicapParking(new DataSourceReader($client), new Wgs84Transformer(), $catalog); $this->entities = array_map( - static fn (NgsiEntity $entity): array => $entity->toArray(['https://example.com/context.jsonld']), + static fn (NgsiEntity $entity): array => $entity->toPayload(['https://example.com/context.jsonld']), iterator_to_array($source->entities(), false) ); } From 8abfaa8c046c3e5c3a5634bacc0cccd362bd92ec Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 9 Sep 2026 14:36:54 +0200 Subject: [PATCH 065/101] Cleaned up --- phpstan.dist.neon | 6 +- src/Command/BrokerEntityDelete.php | 20 -- src/Command/BrokerImportGeoJson.php | 93 ------- src/Command/ImportCommand.php | 128 --------- src/Command/SourceImportCommand.php | 2 - src/Import/DataSourceImporter.php | 136 --------- src/Source/DataSourceReader.php | 58 ---- src/Source/Manifest/Catalog.php | 114 -------- src/Source/Manifest/Descriptor.php | 40 --- src/Source/Manifest/Schema.php | 115 -------- src/Source/Manifest/Validator.php | 34 --- tests/Command/ImportCommandTest.php | 149 ---------- tests/Import/DataSourceImporterTest.php | 259 ------------------ tests/Source/DataSourceReaderTest.php | 114 -------- tests/Source/FakeSource.php | 61 ----- tests/Source/Manifest/CatalogTest.php | 238 ---------------- tests/Source/Manifest/ValidatorTest.php | 90 ------ tests/Source/Manifest/WritesManifests.php | 61 ----- .../MtmSpatialMaps/HandicapParkingTest.php | 190 ------------- 19 files changed, 2 insertions(+), 1906 deletions(-) delete mode 100644 src/Command/BrokerImportGeoJson.php delete mode 100644 src/Command/ImportCommand.php delete mode 100644 src/Import/DataSourceImporter.php delete mode 100644 src/Source/DataSourceReader.php delete mode 100644 src/Source/Manifest/Catalog.php delete mode 100644 src/Source/Manifest/Descriptor.php delete mode 100644 src/Source/Manifest/Schema.php delete mode 100644 src/Source/Manifest/Validator.php delete mode 100644 tests/Command/ImportCommandTest.php delete mode 100644 tests/Import/DataSourceImporterTest.php delete mode 100644 tests/Source/DataSourceReaderTest.php delete mode 100644 tests/Source/FakeSource.php delete mode 100644 tests/Source/Manifest/CatalogTest.php delete mode 100644 tests/Source/Manifest/ValidatorTest.php delete mode 100644 tests/Source/Manifest/WritesManifests.php delete mode 100644 tests/Source/MtmSpatialMaps/HandicapParkingTest.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon index afeb348..0fbcd82 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -5,17 +5,15 @@ parameters: - config/ - public/ - src/ - # - tests/ + - tests/ excludePaths: - src/Kernel.php + - tests/bootstrap.php treatPhpDocTypesAsCertain: false ignoreErrors: - - message: '#Access to protected property proj4php\\Point::\$(x|y).#' - path: src/Command/BrokerImportGeoJson.php - - messages: - "#contains generic class RecursiveArrayIterator but does not specify its types#" - '#Call to an undefined method RecursiveIterator::getArrayCopy\(\).#' diff --git a/src/Command/BrokerEntityDelete.php b/src/Command/BrokerEntityDelete.php index d1da87d..3e585cf 100644 --- a/src/Command/BrokerEntityDelete.php +++ b/src/Command/BrokerEntityDelete.php @@ -9,7 +9,6 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; #[AsCommand( name: 'app:broker:entity:delete', @@ -73,23 +72,4 @@ public function __invoke( return Command::SUCCESS; } - - /** - * Get link URL. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link - */ - private function getLinkUrl(ResponseInterface $response, string $rel): ?string - { - $links = $response->getHeaders()['link'] ?? []; - foreach ($links as $link) { - if (preg_match_all('/<(?P[^>]+)>;\s*rel="(?P[^"]+)"/', (string) $link, $matches, PREG_SET_ORDER)) { - if ($rel === ($matches[0]['rel'] ?? null)) { - return $matches[0]['url'] ?? null; - } - } - } - - return null; - } } diff --git a/src/Command/BrokerImportGeoJson.php b/src/Command/BrokerImportGeoJson.php deleted file mode 100644 index da2839d..0000000 --- a/src/Command/BrokerImportGeoJson.php +++ /dev/null @@ -1,93 +0,0 @@ -request(Request::METHOD_GET, $dataUrl)->toArray(); - $features = $data['features'] ?? null; - $sourceProjection = $data['crs']['properties']['name'] ?? null; - $sourceProjection = null !== $sourceProjection - ? Crs::from($sourceProjection) - : Crs::geoJson(); - - $count = count($features); - if ($count > 0) { - $io->writeln(match ($count) { - 1 => sprintf('Creating one %s entity …', $entityType), - default => sprintf('Creating %d %s entities …', $count, $entityType), - }); - $io->progressStart($count); - foreach ($features as $index => $feature) { - $entity = [ - 'type' => $entityType, - 'id' => sprintf('%s:%04d', $entityType, $index), - ]; - foreach ($feature['properties'] as $name => $value) { - if (null === $value) { - continue; - } - if (in_array($name, ['id', 'type'])) { - $name = self::RESERVED_PROPERTY_NAME_PREFIX.$name; - } - $entity[$name] = [ - 'type' => 'Property', - 'value' => $value, - ]; - } - $geometry = $geometryHelper->transformGeoJsonGeometry($feature['geometry'], from: $sourceProjection); - $entity['geometry'] = $geometry; - $centroid = $geometryHelper->getCentroid($geometry); - // Compute location from geometry. The location must be a single point (the "spatial location" of the entity). - $entity['location'] = [ - 'type' => 'geo:json', - 'value' => [ - 'type' => 'Point', - 'coordinates' => [$centroid->x, $centroid->y], - ], - ]; - $response = $brokerClient->request(Request::METHOD_POST, '/ngsi-ld/v1/entities', [ - 'json' => $entity, - ]); - - if (Response::HTTP_CREATED !== $response->getStatusCode()) { - $io->error($response->getContent(false)); - $io->writeln(json_encode($entity, JSON_PRETTY_PRINT)); - - return Command::FAILURE; - } - $io->progressAdvance(); - } - $io->progressFinish(); - } - - return Command::SUCCESS; - } -} diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php deleted file mode 100644 index e1c4fa9..0000000 --- a/src/Command/ImportCommand.php +++ /dev/null @@ -1,128 +0,0 @@ -addArgument('source', InputArgument::OPTIONAL, 'Source to import. Omit to list the available sources.') - ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Print the NGSI-LD payload instead of sending it.') - ->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Import at most this many entities.'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $keys = $this->importer->keys(); - - if ([] === $keys) { - $io->error('No data sources are registered.'); - $io->listing([ - 'A source must implement App\Source\SourceInterface.', - 'Implementations are picked up automatically — check the class exists and is not excluded from the container.', - ]); - - return Command::FAILURE; - } - - $argument = $input->getArgument('source'); - - if (null === $argument) { - // Options only make sense together with a source. Listing the - // sources and exiting successfully would look like an import ran. - if ($input->getOption('dry-run') || null !== $input->getOption('limit')) { - $io->error(\sprintf( - 'No source given. Available: %s.', - implode(', ', $keys) - )); - - return Command::INVALID; - } - - $io->section('Available sources'); - $io->listing($keys); - - return Command::SUCCESS; - } - - $key = (string) $argument; - $limit = null !== $input->getOption('limit') ? (int) $input->getOption('limit') : null; - - try { - if ($input->getOption('dry-run')) { - $payload = $this->importer->payload($key, $limit); - - $output->writeln(json_encode($payload, self::JSON_FLAGS)); - $io->note(\sprintf('Dry run: %d entities were not sent.', \count($payload))); - - return Command::SUCCESS; - } - - $result = $this->importer->import($key, $limit); - } catch (UnknownSourceException $exception) { - $io->error($exception->getMessage()); - - return Command::INVALID; - } catch (EmptySourceException $exception) { - $io->error($exception->getMessage()); - $io->text( - 'The source ran to completion without raising an exception, so every record was ' - .'discarded by the source\'s own guards rather than failing. Verbosity flags will ' - .'not reveal more: there is no exception to show.' - ); - $io->listing([ - 'Does the configured path or URL point at the intended document?', - 'Does the document match the shape the source expects — envelope, nesting, field names?', - 'Which guard returns early — a missing identifier, or a missing geometry?', - ]); - - return Command::FAILURE; - } catch (UpsertFailedException $exception) { - $io->error($exception->getMessage()); - - return Command::FAILURE; - } - - $io->success(\sprintf( - 'Upserted %d entities into %s (HTTP %d).', - $result->count, - $result->brokerUrl, - $result->status - )); - - return Command::SUCCESS; - } -} diff --git a/src/Command/SourceImportCommand.php b/src/Command/SourceImportCommand.php index 2ab733c..8f5e561 100644 --- a/src/Command/SourceImportCommand.php +++ b/src/Command/SourceImportCommand.php @@ -2,7 +2,6 @@ namespace App\Command; -use App\Source\DataSourceReader; use App\Source\SourceInterface; use App\SourceImporterFactory; use Symfony\Component\Console\Attribute\Argument; @@ -16,7 +15,6 @@ class SourceImportCommand { public function __invoke(SymfonyStyle $io, - DataSourceReader $reader, SourceImporterFactory $factory, #[Argument] SourceInterface $source): int diff --git a/src/Import/DataSourceImporter.php b/src/Import/DataSourceImporter.php deleted file mode 100644 index 597646b..0000000 --- a/src/Import/DataSourceImporter.php +++ /dev/null @@ -1,136 +0,0 @@ - $sources - */ - public function __construct( - #[AutowireIterator('app.source')] - private iterable $sources, - private Catalog $catalog, - private NgsiLdBroker $broker, - #[Autowire(env: 'ENTER_NGSI_CONTEXT_URLS')] - private string $contextUrls, - ) { - } - - /** - * @return list every registered source key, in registration order - */ - public function keys(): array - { - return array_keys($this->registry()); - } - - /** - * Converts a source to NGSI-LD. - * - * @return non-empty-list> - * - * @throws UnknownSourceException when no source is registered under the key - * @throws EmptySourceException when the source yields no entities - */ - public function payload(string $key, ?int $limit = null): array - { - // Collect all dataset keys. - $registry = $this->registry(); - - // Check if requested dataset key exists. - if (!isset($registry[$key])) { - throw new UnknownSourceException($key, array_keys($registry)); - } - - // Define minimum limit, in case of limit defined as less than 1. - $limit = null === $limit ? null : max(1, $limit); - - // Load context for given dataset. - $contexts = $this->contexts($key); - - $payload = []; - foreach ($registry[$key]->entities() as $entity) { - $payload[] = $entity->toArray($contexts); - - // Break upon limit. - if (null !== $limit && \count($payload) >= $limit) { - break; - } - } - - if ([] === $payload) { - throw new EmptySourceException($key); - } - - return $payload; - } - - /** - * @throws UnknownSourceException when no source is registered under the key - * @throws EmptySourceException when the source yields no entities - * @throws UpsertFailedException when the broker cannot be written to - */ - public function import(string $key, ?int $limit = null): ImportResult - { - // Get payload from dataset. - $payload = $this->payload($key, $limit); - - // Try to upsert broker with payload. - try { - $status = $this->broker->upsert($payload); - } catch (\Throwable $exception) { - throw new UpsertFailedException($exception); - } - - // Return result. - return new ImportResult(\count($payload), $status, $this->broker->brokerUrl()); - } - - /** - * Get list of registered datasets. - * - * @see config/sources.yaml - * - * @return array keyed by source key - */ - private function registry(): array - { - $registry = []; - - foreach ($this->sources as $source) { - $registry[$source->id] = $source; - } - - return $registry; - } - - /** - * Return an array of contexts. Each dataset holds its own context. - * - * @see config/sources.yaml - * - * @return array - */ - private function contexts(string $key): array - { - return [ - $this->catalog->get($key)->contextUrl, - ...array_values(array_filter(array_map(trim(...), explode(',', $this->contextUrls)))), - ]; - } -} diff --git a/src/Source/DataSourceReader.php b/src/Source/DataSourceReader.php deleted file mode 100644 index c8f3751..0000000 --- a/src/Source/DataSourceReader.php +++ /dev/null @@ -1,58 +0,0 @@ - the decoded document - * - * @throws \RuntimeException when the location is not an http(s) URL, cannot - * be fetched, does not contain valid JSON, or - * does not decode to an array - */ - public function read(string $url): array - { - if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) { - throw new \RuntimeException(\sprintf('Feed location must be an http(s) URL, got "%s".', $url)); - } - - $json = $this->fetch($url); - - try { - $decoded = json_decode($json, true, 512, \JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new \RuntimeException(\sprintf('Invalid JSON in "%s": %s', $url, $exception->getMessage()), previous: $exception); - } - - // A JSON document may legally be a scalar. Every feed we consume is a - // list or an object, and a scalar here means the location is wrong - // rather than that the feed is empty. - if (!\is_array($decoded)) { - throw new \RuntimeException(\sprintf('Expected a JSON array or object in "%s", got %s.', $url, get_debug_type($decoded))); - } - - return $decoded; - } - - private function fetch(string $url): string - { - try { - return $this->client->request('GET', $url)->getContent(); - } catch (\Throwable $exception) { - throw new \RuntimeException(\sprintf('Could not fetch "%s": %s', $url, $exception->getMessage()), previous: $exception); - } - } -} diff --git a/src/Source/Manifest/Catalog.php b/src/Source/Manifest/Catalog.php deleted file mode 100644 index 71ad47f..0000000 --- a/src/Source/Manifest/Catalog.php +++ /dev/null @@ -1,114 +0,0 @@ -|null */ - private ?array $descriptors = null; - - public function __construct( - #[Autowire('%kernel.project_dir%/config/sources.yaml')] - private readonly string $manifest, - ) { - } - - /** - * @throws \RuntimeException when the manifest cannot be read, or carries no entry for the key - */ - public function get(string $key): Descriptor - { - $descriptors = $this->all(); - - if (!isset($descriptors[$key])) { - throw new \RuntimeException(\sprintf('No entry for source "%s" in %s. Entries: %s.', $key, $this->manifest, [] === $descriptors ? 'none' : implode(', ', array_keys($descriptors)))); - } - - return $descriptors[$key]; - } - - /** - * @return array keyed by source key - * - * @throws \RuntimeException when the manifest cannot be read - */ - public function all(): array - { - return $this->descriptors ??= $this->load(); - } - - /** - * @return array - */ - private function load(): array - { - $descriptors = []; - - foreach ($this->validated() as $key => $entry) { - $descriptors[$key] = new Descriptor( - key: $key, - title: $entry['title'], - accessUrl: $entry['access_url'], - crs: $entry['crs'], - model: $entry['model'], - contextUrl: $entry['context_url'], - description: $entry['description'], - publisher: $entry['publisher'], - contact: $entry['contact'], - landingPage: $entry['landing_page'], - mediaType: $entry['media_type'], - updateFrequency: $entry['update_frequency'], - licence: $entry['licence'], - omittedFields: $entry['omitted_fields'], - ); - } - - return $descriptors; - } - - /** - * @return array}> - */ - private function validated(): array - { - if (!is_file($this->manifest)) { - throw new \RuntimeException(\sprintf('Source manifest "%s" does not exist.', $this->manifest)); - } - - try { - $parsed = Yaml::parseFile($this->manifest); - } catch (ParseException $exception) { - throw new \RuntimeException(\sprintf('Source manifest "%s" is not valid YAML: %s', $this->manifest, $exception->getMessage()), previous: $exception); - } - - $sources = \is_array($parsed) ? $parsed['sources'] ?? null : null; - if (!\is_array($sources)) { - throw new \RuntimeException(\sprintf('Source manifest "%s" must contain a "sources" mapping at the top level.', $this->manifest)); - } - - try { - /** @var array}> $processed */ - $processed = new Processor()->process(Schema::tree(), [$sources]); - } catch (InvalidConfigurationException $exception) { - throw new \RuntimeException(\sprintf('Source manifest "%s" is invalid: %s', $this->manifest, $exception->getMessage()), previous: $exception); - } - - return $processed; - } -} diff --git a/src/Source/Manifest/Descriptor.php b/src/Source/Manifest/Descriptor.php deleted file mode 100644 index 46d0631..0000000 --- a/src/Source/Manifest/Descriptor.php +++ /dev/null @@ -1,40 +0,0 @@ - $omittedFields source field name => why it is not published - */ - public function __construct( - public string $key, - public string $title, - public string $accessUrl, - public string $crs, - public string $model, - public string $contextUrl, - public ?string $description = null, - public ?string $publisher = null, - public ?string $contact = null, - public ?string $landingPage = null, - public ?string $mediaType = null, - public ?string $updateFrequency = null, - public ?string $licence = null, - public array $omittedFields = [], - ) { - } -} diff --git a/src/Source/Manifest/Schema.php b/src/Source/Manifest/Schema.php deleted file mode 100644 index bbaf41c..0000000 --- a/src/Source/Manifest/Schema.php +++ /dev/null @@ -1,115 +0,0 @@ -getRootNode() - // The import selects a data set by its key exactly as written in - // the manifest. Key normalization rewrites a key that contains - // dashes and no underscore, which breaks that lookup silently. - ->normalizeKeys(false) - ->requiresAtLeastOneElement() - ->arrayPrototype() - ->beforeNormalization() - ->always(self::blankToNull()) - ->end() - ->children() - ->scalarNode('title') - ->isRequired() - ->cannotBeEmpty() - ->info('What the data set is called where it is published.') - ->end() - ->scalarNode('access_url') - ->isRequired() - ->cannotBeEmpty() - ->info('Where the feed is read from; an import cannot run without it.') - ->end() - ->scalarNode('crs') - ->isRequired() - ->cannotBeEmpty() - ->info('The CRS the feed publishes coordinates in, e.g. "EPSG:25832". A wrong value yields well-formed coordinates in the wrong place.') - ->end() - ->scalarNode('model') - ->isRequired() - ->cannotBeEmpty() - ->info('Smart Data Model the data set is published as.') - ->end() - ->scalarNode('context_url') - ->isRequired() - ->cannotBeEmpty() - ->info('JSON-LD context defining the model\'s terms. Without the right one a consumer reads the entity\'s attributes as undefined strings.') - ->end() - ->scalarNode('description')->defaultNull()->end() - ->scalarNode('publisher')->defaultNull()->end() - ->scalarNode('contact')->defaultNull()->end() - ->scalarNode('landing_page')->defaultNull()->end() - ->scalarNode('media_type')->defaultNull()->end() - ->scalarNode('update_frequency')->defaultNull()->end() - ->scalarNode('licence') - ->defaultNull() - ->info('Empty means the terms are unsettled; DCAT-AP requires one before the data set can be registered.') - ->end() - ->arrayNode('omitted_fields') - ->defaultValue([]) - ->normalizeKeys(false) - ->scalarPrototype() - ->beforeNormalization() - ->always(self::blankToNullValue()) - ->end() - ->cannotBeEmpty() - ->info('The reason a field is withheld cannot be recovered from the code, so a bare list of names is not accepted.') - ->end() - ->end() - ->end() - ->end(); - - return $tree->buildTree(); - } - - /** - * An empty value means "not filled in", the same as an absent key, so both - * become null rather than an empty string. - * - * Normalization runs before the type check, so an entry that is not a - * mapping has to pass through untouched for the tree to report it as one. - */ - private static function blankToNull(): \Closure - { - return static function (mixed $entry): mixed { - if (!\is_array($entry)) { - return $entry; - } - - return array_map(self::blankToNullValue(), $entry); - }; - } - - private static function blankToNullValue(): \Closure - { - return static function (mixed $value): mixed { - if (!\is_string($value)) { - return $value; - } - - $value = trim($value); - - return '' === $value ? null : $value; - }; - } -} diff --git a/src/Source/Manifest/Validator.php b/src/Source/Manifest/Validator.php deleted file mode 100644 index 01d54dd..0000000 --- a/src/Source/Manifest/Validator.php +++ /dev/null @@ -1,34 +0,0 @@ - nothing is written, so nothing is preloaded - */ - public function warmUp(string $cacheDir, ?string $buildDir = null): array - { - $this->catalog->all(); - - return []; - } -} diff --git a/tests/Command/ImportCommandTest.php b/tests/Command/ImportCommandTest.php deleted file mode 100644 index 828e17f..0000000 --- a/tests/Command/ImportCommandTest.php +++ /dev/null @@ -1,149 +0,0 @@ - $sources - */ - private function tester(iterable $sources, ?MockHttpClient $client = null): CommandTester - { - $keys = []; - foreach ($sources as $source) { - $keys[] = $source->id; - } - - return new CommandTester(new ImportCommand(new DataSourceImporter( - $sources, - new Catalog($this->manifestFor($keys)), - new NgsiLdBroker($client ?? new MockHttpClient(), 'http://broker.invalid'), - 'https://example.com/core.jsonld', - ))); - } - - public function testItListsTheSourcesWhenCalledBare(): void - { - $tester = $this->tester([FakeSource::withEntities('some-source')]); - - $status = $tester->execute([]); - - $this->assertSame(Command::SUCCESS, $status); - $this->assertStringContainsString('some-source', $tester->getDisplay()); - } - - public function testItFailsWhenNoSourcesAreRegistered(): void - { - $tester = $this->tester([]); - - $status = $tester->execute([]); - - $this->assertSame(Command::FAILURE, $status); - $this->assertStringContainsString('No data sources are registered', $tester->getDisplay()); - } - - /** - * Passing --dry-run with no source used to print the source listing and - * exit successfully, silently ignoring the flag. - */ - public function testItRejectsOptionsWithoutASource(): void - { - $tester = $this->tester([FakeSource::withEntities('some-source')]); - - $status = $tester->execute(['--dry-run' => true]); - - $this->assertSame(Command::INVALID, $status); - $this->assertStringContainsString('No source given', $tester->getDisplay()); - } - - public function testAnUnknownSourceIsTheCallersMistake(): void - { - $tester = $this->tester([FakeSource::withEntities('some-source')]); - - $status = $tester->execute(['source' => 'nope']); - - $this->assertSame(Command::INVALID, $status); - $this->assertStringContainsString('Unknown source "nope"', $tester->getDisplay()); - } - - /** - * The failure carries no exception to show, so the command has to supply - * the places worth looking itself. - */ - public function testAnEmptySourceFailsAndSuggestsCauses(): void - { - $tester = $this->tester([new FakeSource('empty-source')]); - - $status = $tester->execute(['source' => 'empty-source']); - $display = $tester->getDisplay(); - - $this->assertSame(Command::FAILURE, $status); - $this->assertStringContainsString('produced no entities', $display); - $this->assertStringContainsString('path or URL', $display); - $this->assertStringContainsString('envelope, nesting, field names', $display); - } - - public function testDryRunPrintsThePayloadAndSendsNothing(): void - { - $client = new MockHttpClient(); - $tester = $this->tester([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], $client); - - $status = $tester->execute(['source' => 'one-entity', '--dry-run' => true]); - $display = $tester->getDisplay(); - - $this->assertSame(Command::SUCCESS, $status); - $this->assertStringContainsString('"urn:ngsi-ld:Example:1"', $display); - $this->assertStringContainsString('1 entities were not sent', $display); - $this->assertSame(0, $client->getRequestsCount()); - } - - public function testItReportsWhatWasUpserted(): void - { - $client = new MockHttpClient(new MockResponse('', ['http_code' => 204])); - $tester = $this->tester([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], $client); - - $status = $tester->execute(['source' => 'one-entity']); - $display = $tester->getDisplay(); - - $this->assertSame(Command::SUCCESS, $status); - $this->assertStringContainsString('Upserted 1 entities', $display); - $this->assertStringContainsString('HTTP 204', $display); - } - - /** - * The broker being down is an operational condition rather than a bug, so - * it is reported as a message instead of an uncaught exception. - */ - public function testABrokerFailureIsReportedAsAnError(): void - { - $client = new MockHttpClient(new MockResponse('', ['http_code' => 500])); - $tester = $this->tester([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], $client); - - $status = $tester->execute(['source' => 'one-entity']); - - $this->assertSame(Command::FAILURE, $status); - $this->assertStringContainsString('HTTP 500', $tester->getDisplay()); - } -} diff --git a/tests/Import/DataSourceImporterTest.php b/tests/Import/DataSourceImporterTest.php deleted file mode 100644 index 6686577..0000000 --- a/tests/Import/DataSourceImporterTest.php +++ /dev/null @@ -1,259 +0,0 @@ - $sources each is registered in the manifest under its own key - */ - private function importer( - iterable $sources, - ?MockHttpClient $client = null, - string $contextUrls = self::CORE_CONTEXT, - ): DataSourceImporter { - $keys = []; - foreach ($sources as $source) { - $keys[] = $source->id; - } - - return new DataSourceImporter( - $sources, - new Catalog($this->manifestFor($keys)), - new NgsiLdBroker($client ?? new MockHttpClient(), self::BROKER_URL), - $contextUrls, - ); - } - - public function testItListsTheRegisteredSourceKeys(): void - { - $importer = $this->importer([FakeSource::withEntities('a-source'), FakeSource::withEntities('b-source')]); - - $this->assertSame(['a-source', 'b-source'], $importer->keys()); - } - - public function testItListsNothingWhenNoSourceIsRegistered(): void - { - $this->assertSame([], $this->importer([])->keys()); - } - - public function testItRejectsAnUnknownSourceAndNamesTheKnownOnes(): void - { - $importer = $this->importer([FakeSource::withEntities('some-source')]); - - $this->expectException(UnknownSourceException::class); - $this->expectExceptionMessage('Unknown source "nope". Available: some-source.'); - - $importer->payload('nope'); - } - - /** - * The important one: a source yielding nothing used to be reported as a - * completed import, which is indistinguishable from a working one. - */ - public function testItFailsWhenASourceProducesNothing(): void - { - $importer = $this->importer([new FakeSource('empty-source')]); - - $this->expectException(EmptySourceException::class); - $this->expectExceptionMessage('Source "empty-source" produced no entities.'); - - $importer->payload('empty-source'); - } - - /** - * The guard belongs to the conversion rather than to the dry run, so an - * import that intends to send is held to it too. - */ - public function testItSendsNothingWhenASourceProducesNothing(): void - { - $client = new MockHttpClient(); - - try { - $this->importer([new FakeSource('empty-source')], $client)->import('empty-source'); - $this->fail('An empty source was reported as a completed import.'); - } catch (EmptySourceException) { - $this->assertSame(0, $client->getRequestsCount(), 'An empty payload was sent to the broker.'); - } - } - - /** - * Later entries win term conflicts, so the data set's own context comes - * first and the core context last, where it stays authoritative over the - * NGSI-LD terms a domain context may also define. - */ - public function testEveryEntityCarriesItsDataSetContextThenTheCoreContext(): void - { - $importer = $this->importer([FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')]); - - $payload = $importer->payload('one-entity'); - - $this->assertSame( - [self::dataSetContext('one-entity'), self::CORE_CONTEXT], - $payload[0]['@context'] - ); - } - - /** - * Two data sets published under different models must not advertise each - * other's vocabulary, which a single app-wide list cannot avoid. - */ - public function testEachDataSetCarriesOnlyItsOwnContext(): void - { - $importer = $this->importer([ - FakeSource::withEntities('first-source', 'urn:ngsi-ld:Example:1'), - FakeSource::withEntities('second-source', 'urn:ngsi-ld:Example:2'), - ]); - - $this->assertSame( - [self::dataSetContext('first-source'), self::CORE_CONTEXT], - $importer->payload('first-source')[0]['@context'] - ); - $this->assertSame( - [self::dataSetContext('second-source'), self::CORE_CONTEXT], - $importer->payload('second-source')[0]['@context'] - ); - } - - /** - * A source registered in the container but not in the manifest has no - * context to publish under, so it fails rather than emitting entities a - * consumer cannot resolve. - */ - public function testItFailsWhenASourceHasNoManifestEntry(): void - { - $importer = new DataSourceImporter( - [FakeSource::withEntities('unregistered', 'urn:ngsi-ld:Example:1')], - new Catalog($this->manifestFor(['other-source'])), - new NgsiLdBroker(new MockHttpClient(), self::BROKER_URL), - self::CORE_CONTEXT, - ); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('No entry for source "unregistered"'); - - $importer->payload('unregistered'); - } - - /** - * The contexts arrive as one comma-separated environment variable, so they - * are written by hand and carry whatever spacing that produces. - */ - public function testItIgnoresSpacingAndEmptyEntriesInTheConfiguredContexts(): void - { - $importer = $this->importer( - [FakeSource::withEntities('one-entity', 'urn:ngsi-ld:Example:1')], - contextUrls: ' '.self::CORE_CONTEXT.' , ,', - ); - - $payload = $importer->payload('one-entity'); - - $this->assertSame( - [self::dataSetContext('one-entity'), self::CORE_CONTEXT], - $payload[0]['@context'] - ); - } - - public function testALimitCapsThePayload(): void - { - $importer = $this->importer([FakeSource::withEntities('many', 'urn:1', 'urn:2', 'urn:3', 'urn:4', 'urn:5')]); - - $this->assertCount(2, $importer->payload('many', 2)); - } - - /** - * A real source reads a whole feed, so a limit that converts everything - * and then trims would do all the work it was given to avoid. - */ - public function testALimitStopsPullingFromTheSource(): void - { - $source = FakeSource::withEntities('many', 'urn:1', 'urn:2', 'urn:3', 'urn:4', 'urn:5'); - - $this->importer([$source])->payload('many', 2); - - $this->assertSame(2, $source->produced()); - } - - /** - * --limit 0 is a mistyped option. Honouring it would produce an empty - * payload, which is the one outcome an import refuses to call a success. - */ - public function testALimitBelowOneStillImportsOneEntity(): void - { - $importer = $this->importer([FakeSource::withEntities('many', 'urn:1', 'urn:2')]); - - $this->assertCount(1, $importer->payload('many', 0)); - } - - public function testBuildingThePayloadSendsNothing(): void - { - $client = new MockHttpClient(); - - $this->importer([FakeSource::withEntities('one-entity', 'urn:1')], $client)->payload('one-entity'); - - $this->assertSame(0, $client->getRequestsCount()); - } - - public function testItUpsertsThePayloadAndReportsWhatTheBrokerDid(): void - { - $client = new MockHttpClient(new MockResponse('', ['http_code' => 204])); - - $result = $this->importer([FakeSource::withEntities('many', 'urn:1', 'urn:2')], $client)->import('many'); - - $this->assertSame(2, $result->count); - $this->assertSame(204, $result->status); - $this->assertSame(self::BROKER_URL, $result->brokerUrl); - $this->assertSame(1, $client->getRequestsCount(), 'The entities were not sent as one batch.'); - } - - public function testItReportsWhatTheBrokerSaidWhenTheUpsertIsRejected(): void - { - $client = new MockHttpClient(new MockResponse('{"title":"Bad Request"}', ['http_code' => 400])); - - try { - $this->importer([FakeSource::withEntities('one-entity', 'urn:1')], $client)->import('one-entity'); - $this->fail('A rejected upsert was reported as a completed import.'); - } catch (UpsertFailedException $exception) { - $this->assertStringContainsString('HTTP 400', $exception->getMessage()); - $this->assertStringContainsString('Bad Request', $exception->getMessage()); - $this->assertNotNull($exception->getPrevious(), 'The broker\'s own exception was discarded.'); - } - } - - /** - * A broker that cannot be reached fails in the HTTP client rather than in - * the broker's status check, and the two are the same thing to a caller. - */ - public function testItFailsTheSameWayWhenTheBrokerCannotBeReached(): void - { - $client = new MockHttpClient(static function (): never { - throw new TransportException('Connection refused'); - }); - - $this->expectException(UpsertFailedException::class); - $this->expectExceptionMessage('Connection refused'); - - $this->importer([FakeSource::withEntities('one-entity', 'urn:1')], $client)->import('one-entity'); - } -} diff --git a/tests/Source/DataSourceReaderTest.php b/tests/Source/DataSourceReaderTest.php deleted file mode 100644 index 9c866ca..0000000 --- a/tests/Source/DataSourceReaderTest.php +++ /dev/null @@ -1,114 +0,0 @@ -reader($client)->read('https://example.com/feed.json'); - - $this->assertSame([['example' => 1]], $document); - } - - public function testItAcceptsABareArrayDocument(): void - { - $client = new MockHttpClient(new MockResponse('[1, 2, 3]')); - - $this->assertSame([1, 2, 3], $this->reader($client)->read('http://example.com/feed.json')); - } - - /** - * The envelope is feed-specific knowledge, so it must survive intact for - * the caller to interpret. Returning the features list directly would push - * that knowledge into the wrong class. - */ - public function testItDoesNotUnwrapTheEnvelope(): void - { - $client = new MockHttpClient(new MockResponse(self::FEATURE_COLLECTION)); - - $document = $this->reader($client)->read('https://example.com/feed.json'); - - $this->assertArrayHasKey('features', $document); - $this->assertArrayNotHasKey(0, $document); - $this->assertCount(2, $document['features']); - } - - public function testItRejectsALocationThatIsNotAUrl(): void - { - $client = new MockHttpClient(); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/must be an http\(s\) URL/'); - - try { - $this->reader($client)->read('data/feed.json'); - } finally { - // Rejection has to happen before the request, or a mistyped - // location becomes an opaque transport error instead. - $this->assertSame(0, $client->getRequestsCount()); - } - } - - /** - * A string that merely begins with "http" is not a URL. - */ - public function testItRejectsAPathThatMerelyStartsWithHttp(): void - { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/must be an http\(s\) URL/'); - - $this->reader()->read('https-export.json'); - } - - public function testItFailsOnInvalidJson(): void - { - $client = new MockHttpClient(new MockResponse('{ not json')); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/Invalid JSON/'); - - $this->reader($client)->read('https://example.com/feed.json'); - } - - public function testItFailsWhenTheDocumentIsAScalar(): void - { - $client = new MockHttpClient(new MockResponse('"just a string"')); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/Expected a JSON array or object/'); - - $this->reader($client)->read('https://example.com/feed.json'); - } - - public function testItWrapsTransportFailures(): void - { - $client = new MockHttpClient(new MockResponse('', ['http_code' => 500])); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/Could not fetch/'); - - $this->reader($client)->read('https://example.com/feed.json'); - } -} diff --git a/tests/Source/FakeSource.php b/tests/Source/FakeSource.php deleted file mode 100644 index 052ab33..0000000 --- a/tests/Source/FakeSource.php +++ /dev/null @@ -1,61 +0,0 @@ - $entities - */ - public function __construct( - private readonly string $key, - private readonly array $entities = [], - ) { - } - - /** - * @param string ...$ids entity ids, one entity each - */ - public static function withEntities(string $key, string ...$ids): self - { - return new self($key, array_map( - static fn (string $id): NgsiEntity => new NgsiEntity($id, 'Example'), - array_values($ids) - )); - } - - public function key(): string - { - return $this->key; - } - - public function entities(): iterable - { - foreach ($this->entities as $entity) { - ++$this->produced; - - yield $entity; - } - } - - /** - * How many entities were actually pulled from this source. - */ - public function produced(): int - { - return $this->produced; - } -} diff --git a/tests/Source/Manifest/CatalogTest.php b/tests/Source/Manifest/CatalogTest.php deleted file mode 100644 index 2707d49..0000000 --- a/tests/Source/Manifest/CatalogTest.php +++ /dev/null @@ -1,238 +0,0 @@ - */ - private array $written = []; - - protected function tearDown(): void - { - foreach ($this->written as $path) { - if (is_file($path)) { - unlink($path); - } - } - - $this->written = []; - } - - public function testTheShippedManifestIsUsable(): void - { - $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); - - $this->assertNotSame([], $catalog->all(), 'The manifest registers no data sets.'); - } - - /** - * A wrong URL scheme or a CRS the transformer does not know only surfaces - * mid-import otherwise, after the feed has been fetched. - */ - public function testEveryShippedEntryCanBeImportedFrom(): void - { - $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); - - foreach ($catalog->all() as $key => $descriptor) { - $this->assertSame($key, $descriptor->key); - $this->assertMatchesRegularExpression('#^https?://#', $descriptor->accessUrl, $key); - $this->assertMatchesRegularExpression('/^EPSG:\d+$/', $descriptor->crs, $key); - $this->assertNotSame('', $descriptor->model, $key); - $this->assertMatchesRegularExpression('#^https?://#', $descriptor->contextUrl, $key); - } - } - - public function testItNamesTheKnownEntriesWhenAskedForAnUnknownOne(): void - { - $catalog = new Catalog($this->manifest(<<<'YAML' - sources: - a-source: - title: A source - access_url: https://example.com/feed.json - crs: EPSG:25832 - model: Example - context_url: https://example.com/context.jsonld - YAML)); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Entries: a-source.'); - - $catalog->get('no-such-source'); - } - - public function testItRejectsAManifestWithoutASourcesMapping(): void - { - $catalog = new Catalog($this->manifest("data_sets:\n a-source: {}\n")); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('must contain a "sources" mapping'); - - $catalog->all(); - } - - public function testItRejectsAnEntryMissingAFieldTheImportNeeds(): void - { - $catalog = new Catalog($this->manifest(<<<'YAML' - sources: - a-source: - title: A source - access_url: https://example.com/feed.json - model: Example - context_url: https://example.com/context.jsonld - YAML)); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('The child config "crs" under "sources.a-source" must be configured'); - - $catalog->all(); - } - - public function testItRejectsAnOmittedFieldWithoutAReason(): void - { - $catalog = new Catalog($this->manifest(<<<'YAML' - sources: - a-source: - title: A source - access_url: https://example.com/feed.json - crs: EPSG:25832 - model: Example - context_url: https://example.com/context.jsonld - omitted_fields: - some_field: ~ - YAML)); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('"sources.a-source.omitted_fields.some_field" cannot contain an empty value'); - - $catalog->all(); - } - - /** - * The import selects a data set by the key written in the manifest, and the - * config tree rewrites a key that has dashes and no underscore unless told - * otherwise. A rewritten key stops matching without saying so. - */ - public function testItKeepsADashedSourceKeyIntact(): void - { - $catalog = new Catalog($this->manifest(<<<'YAML' - sources: - handicap-parking: - title: A source - access_url: https://example.com/feed.json - crs: EPSG:25832 - model: Example - context_url: https://example.com/context.jsonld - YAML)); - - $this->assertSame(['handicap-parking'], array_keys($catalog->all())); - $this->assertSame('handicap-parking', $catalog->get('handicap-parking')->key); - } - - /** - * A misspelled optional field was dropped in silence before, which loses a - * fact the record exists to carry. - */ - public function testItRejectsAFieldTheManifestDoesNotDefine(): void - { - $catalog = new Catalog($this->manifest(<<<'YAML' - sources: - a-source: - title: A source - access_url: https://example.com/feed.json - crs: EPSG:25832 - model: Example - context_url: https://example.com/context.jsonld - license: CC-BY-4.0 - YAML)); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Unrecognized option "license" under "sources.a-source"'); - - $catalog->all(); - } - - public function testItRejectsAnEntryThatIsNotAMapping(): void - { - $catalog = new Catalog($this->manifest("sources:\n a-source: just a string\n")); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Invalid type for path "sources.a-source"'); - - $catalog->all(); - } - - public function testItRejectsAManifestThatRegistersNothing(): void - { - $catalog = new Catalog($this->manifest("sources: {}\n")); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('should have at least 1 element'); - - $catalog->all(); - } - - /** - * An empty value means "not filled in", the same as an absent key, so an - * unanswered question reads the same either way. - */ - public function testItReadsABlankOptionalFieldAsUnknown(): void - { - $catalog = new Catalog($this->manifest(<<<'YAML' - sources: - a-source: - title: A source - access_url: https://example.com/feed.json - crs: EPSG:25832 - model: Example - context_url: https://example.com/context.jsonld - publisher: ' Aarhus Kommune ' - contact: '' - licence: ~ - YAML)); - - $descriptor = $catalog->get('a-source'); - - $this->assertSame('Aarhus Kommune', $descriptor->publisher); - $this->assertNull($descriptor->contact); - $this->assertNull($descriptor->licence); - } - - public function testItReportsAManifestThatIsNotThere(): void - { - $catalog = new Catalog('/no/such/sources.yaml'); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('does not exist'); - - $catalog->all(); - } - - public function testItReportsUnparsableYaml(): void - { - $catalog = new Catalog($this->manifest("sources:\n - [unbalanced\n")); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('is not valid YAML'); - - $catalog->all(); - } - - private function manifest(string $yaml): string - { - $path = tempnam(sys_get_temp_dir(), 'sources-'); - - if (false === $path) { - $this->fail('Could not create a temporary manifest.'); - } - - file_put_contents($path, $yaml); - $this->written[] = $path; - - return $path; - } -} diff --git a/tests/Source/Manifest/ValidatorTest.php b/tests/Source/Manifest/ValidatorTest.php deleted file mode 100644 index 53230bb..0000000 --- a/tests/Source/Manifest/ValidatorTest.php +++ /dev/null @@ -1,90 +0,0 @@ - */ - private array $written = []; - - protected function tearDown(): void - { - foreach ($this->written as $path) { - if (is_file($path)) { - unlink($path); - } - } - - $this->written = []; - } - - /** - * A check that can be skipped is not a check. - */ - public function testItIsNotOptional(): void - { - $this->assertFalse($this->validator(\dirname(__DIR__, 3).'/config/sources.yaml')->isOptional()); - } - - /** - * It validates rather than caches, so it leaves nothing behind to preload. - */ - public function testItAcceptsTheShippedManifestAndWritesNothing(): void - { - $validator = $this->validator(\dirname(__DIR__, 3).'/config/sources.yaml'); - - $this->assertSame([], $validator->warmUp(sys_get_temp_dir(), sys_get_temp_dir())); - } - - /** - * The reason for checking at build time: an entry no import selects still - * fails the build rather than waiting to be discovered. - */ - public function testItFailsOnAnEntryNoImportWouldReach(): void - { - $validator = $this->validator($this->manifest(<<<'YAML' - sources: - a-source: - title: A source - access_url: https://example.com/feed.json - crs: EPSG:25832 - model: Example - context_url: https://example.com/context.jsonld - unreached-source: - title: Another source - access_url: https://example.com/other.json - model: Example - context_url: https://example.com/context.jsonld - YAML)); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('The child config "crs" under "sources.unreached-source" must be configured'); - - $validator->warmUp(sys_get_temp_dir(), sys_get_temp_dir()); - } - - private function validator(string $manifest): Validator - { - return new Validator(new Catalog($manifest)); - } - - private function manifest(string $yaml): string - { - $path = tempnam(sys_get_temp_dir(), 'sources-'); - - if (false === $path) { - $this->fail('Could not create a temporary manifest.'); - } - - file_put_contents($path, $yaml); - $this->written[] = $path; - - return $path; - } -} diff --git a/tests/Source/Manifest/WritesManifests.php b/tests/Source/Manifest/WritesManifests.php deleted file mode 100644 index 121509f..0000000 --- a/tests/Source/Manifest/WritesManifests.php +++ /dev/null @@ -1,61 +0,0 @@ - */ - private array $manifests = []; - - protected function tearDown(): void - { - foreach ($this->manifests as $path) { - if (is_file($path)) { - unlink($path); - } - } - - $this->manifests = []; - } - - protected static function dataSetContext(string $key): string - { - return \sprintf('https://example.com/%s.jsonld', $key); - } - - /** - * @param list $keys - * - * @return string path to the manifest - */ - protected function manifestFor(array $keys): string - { - $entries = array_map(static fn (string $key): string => \sprintf( - " %s:\n title: %s\n access_url: https://example.com/%s.json\n crs: EPSG:25832\n model: Example\n context_url: %s", - $key, - $key, - $key, - self::dataSetContext($key), - ), $keys); - - $path = tempnam(sys_get_temp_dir(), 'sources-'); - - if (false === $path) { - $this->fail('Could not create a temporary manifest.'); - } - - file_put_contents($path, "sources:\n".implode("\n", $entries)."\n"); - $this->manifests[] = $path; - - return $path; - } -} diff --git a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php b/tests/Source/MtmSpatialMaps/HandicapParkingTest.php deleted file mode 100644 index ff6b8f9..0000000 --- a/tests/Source/MtmSpatialMaps/HandicapParkingTest.php +++ /dev/null @@ -1,190 +0,0 @@ -> */ - private array $entities; - - protected function setUp(): void - { - $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); - $this->source = $catalog->get(self::KEY); - - $client = new MockHttpClient(function (string $method, string $url): MockResponse { - $this->requestedUrl = $url; - - return new MockResponse(json_encode($this->feed(), \JSON_THROW_ON_ERROR)); - }); - - $source = new HandicapParking(new DataSourceReader($client), new Wgs84Transformer(), $catalog); - - $this->entities = array_map( - static fn (NgsiEntity $entity): array => $entity->toPayload(['https://example.com/context.jsonld']), - iterator_to_array($source->entities(), false) - ); - } - - public function testItReadsTheFeedTheManifestPointsAt(): void - { - $this->assertSame($this->source->accessUrl, $this->requestedUrl); - } - - public function testItSkipsRecordsWithoutAnIdentifierOrGeometry(): void - { - // Four features, of which one has no mi_prinx and one no geometry. - $this->assertCount(2, $this->entities); - } - - public function testItTakesIdentifierAndTypeFromTheManifestModel(): void - { - $first = $this->entities[0]; - - // The id is derived from mi_prinx so that re-importing upserts the - // same bay instead of creating a duplicate. - $this->assertSame( - \sprintf('urn:ngsi-ld:%s:aarhus-handicap-172', $this->source->model), - $first['id'] - ); - $this->assertSame($this->source->model, $first['type']); - } - - public function testItJoinsStreetAndHouseNumberIntoName(): void - { - $this->assertSame('Domkirkeplads/Bispegade 1', $this->entities[0]['name']['value']); - } - - public function testItOmitsTheHouseNumberWhenBlank(): void - { - // husnnr is "" for this row, so the name must not end in a space. - $this->assertSame('Brammersgade', $this->entities[1]['name']['value']); - } - - public function testItMarksEveryEntityAsDisabledParking(): void - { - foreach ($this->entities as $entity) { - $this->assertSame(['forDisabled'], $entity['category']['value']); - } - } - - public function testItCarriesTheBayCountAsTotalSpotNumber(): void - { - $this->assertSame(6, $this->entities[0]['totalSpotNumber']['value']); - $this->assertSame(1, $this->entities[1]['totalSpotNumber']['value']); - } - - public function testItDropsEmptyDescriptions(): void - { - // bemrk is null for the first row and filled in for the second. - $this->assertArrayNotHasKey('description', $this->entities[0]); - $this->assertSame('Ved indgangen', $this->entities[1]['description']['value']); - } - - public function testItReprojectsLocationIntoWgs84(): void - { - $location = $this->entities[0]['location']; - - $this->assertSame('GeoProperty', $location['type']); - $this->assertSame('Point', $location['value']['type']); - - // The feed publishes metres in the manifest's CRS, so degrees within - // Denmark are the evidence that the reprojection ran. - [$longitude, $latitude] = $location['value']['coordinates']; - $this->assertGreaterThan(8.0, $longitude); - $this->assertLessThan(13.0, $longitude); - $this->assertGreaterThan(54.5, $latitude); - $this->assertLessThan(57.8, $latitude); - } - - public function testItRecordsTheManifestUrlAsTheEntitySource(): void - { - $this->assertSame($this->source->accessUrl, $this->entities[0]['source']['value']); - } - - /** - * The first feature is a record from the live export, kept verbatim. The - * rest are constructed to exercise a blank house number and the two guards - * that discard a record. - * - * @return array - */ - private function feed(): array - { - return [ - 'type' => 'FeatureCollection', - 'crs' => ['type' => 'name', 'properties' => ['name' => 'EPSG:25832']], - 'features' => [ - [ - 'type' => 'Feature', - 'geometry' => ['type' => 'Point', 'coordinates' => [575153.9524951308, 6224260.609753487]], - 'properties' => [ - 'vejnavn' => 'Domkirkeplads/Bispegade', - 'husnnr' => '1', - 'invalidepladser' => 6, - 'bemrk' => null, - 'ident' => 'P', - 'oprettet_af' => 'ADM\\aztnbnd', - 'oprettet_dato' => '2019-03-19 14:51:23.91', - 'rettet_af' => 'ADM\\aztnbnd', - 'rettet_dato' => '2019-03-19 14:51:23.91', - 'mi_style' => null, - 'mi_prinx' => 172, - ], - ], - [ - 'type' => 'Feature', - 'geometry' => ['type' => 'Point', 'coordinates' => [574000.0, 6223000.0]], - 'properties' => [ - 'vejnavn' => 'Brammersgade', - 'husnnr' => '', - 'invalidepladser' => 1, - 'bemrk' => 'Ved indgangen', - 'mi_prinx' => 378, - ], - ], - [ - 'type' => 'Feature', - 'geometry' => ['type' => 'Point', 'coordinates' => [574100.0, 6223100.0]], - 'properties' => [ - 'vejnavn' => 'Uden nøgle', - 'husnnr' => '3', - 'invalidepladser' => 2, - ], - ], - [ - 'type' => 'Feature', - 'geometry' => null, - 'properties' => [ - 'vejnavn' => 'Uden geometri', - 'husnnr' => '5', - 'invalidepladser' => 2, - 'mi_prinx' => 999, - ], - ], - ], - ]; - } -} From 0a7408453b3a67c2573e20159362b9be7b5bb973 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 9 Sep 2026 14:38:19 +0200 Subject: [PATCH 066/101] Moved stuff around --- CHANGELOG.md | 2 ++ src/Command/SourceImportCommand.php | 2 +- src/{ => SourceImporter}/SourceImporterFactory.php | 2 +- src/{ => SourceImporter}/SourceImporterGeoJson.php | 3 ++- src/{ => SourceImporter}/SourceImporterInterface.php | 2 +- src/{ => SourceReader}/SourceReaderGeoJson.php | 2 +- src/{ => SourceReader}/SourceReaderInterface.php | 2 +- 7 files changed, 9 insertions(+), 6 deletions(-) rename src/{ => SourceImporter}/SourceImporterFactory.php (96%) rename src/{ => SourceImporter}/SourceImporterGeoJson.php (97%) rename src/{ => SourceImporter}/SourceImporterInterface.php (93%) rename src/{ => SourceReader}/SourceReaderGeoJson.php (97%) rename src/{ => SourceReader}/SourceReaderInterface.php (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c67b120..9a8dae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-9](https://github.com/itk-dev/enter/pull/9) + Refactored source import * [PR-7](https://github.com/itk-dev/enter/pull/7) Refactored source definition * [#3](https://github.com/itk-dev/enter/pull/3) diff --git a/src/Command/SourceImportCommand.php b/src/Command/SourceImportCommand.php index 8f5e561..94945a7 100644 --- a/src/Command/SourceImportCommand.php +++ b/src/Command/SourceImportCommand.php @@ -3,7 +3,7 @@ namespace App\Command; use App\Source\SourceInterface; -use App\SourceImporterFactory; +use App\SourceImporter\SourceImporterFactory; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; diff --git a/src/SourceImporterFactory.php b/src/SourceImporter/SourceImporterFactory.php similarity index 96% rename from src/SourceImporterFactory.php rename to src/SourceImporter/SourceImporterFactory.php index aee58ea..4dce0d3 100644 --- a/src/SourceImporterFactory.php +++ b/src/SourceImporter/SourceImporterFactory.php @@ -1,6 +1,6 @@ Date: Fri, 11 Sep 2026 12:24:26 +0200 Subject: [PATCH 067/101] Added a Definition value object --- src/Source/Definition.php | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/Source/Definition.php diff --git a/src/Source/Definition.php b/src/Source/Definition.php new file mode 100644 index 0000000..0758db7 --- /dev/null +++ b/src/Source/Definition.php @@ -0,0 +1,57 @@ + $omittedFields + */ + public function __construct( + public string $id, + public string $title, + public string $description, + public string $publisher, + public string $contact, + public string $landingPage, + public string $accessUrl, + public string $mediaType, + public string $crs, + public string $model, + public string $contextUrl, + public string $updateFrequency, + public ?string $licence, + public array $omittedFields, + ) { + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'publisher' => $this->publisher, + 'contact' => $this->contact, + 'landing_page' => $this->landingPage, + 'access_url' => $this->accessUrl, + 'media_type' => $this->mediaType, + 'crs' => $this->crs, + 'model' => $this->model, + 'context_url' => $this->contextUrl, + 'update_frequency' => $this->updateFrequency, + 'licence' => $this->licence, + 'omitted_fields' => $this->omittedFields, + ]; + } +} From 1d90a67b51f2e86b5eaf574ff9bfb09479f625c7 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 11 Sep 2026 12:24:26 +0200 Subject: [PATCH 068/101] Moved the source configuration into the adapter The adapter builds its own Definition in the constructor. SourceInterface requires the property, so an adapter that fails to declare it cannot be loaded, and one that declares it without assigning it fails static analysis. --- src/Source/AbstractSource.php | 44 +------------- src/Source/MtmSpatialMaps/HandicapParking.php | 14 +++-- src/Source/SourceInterface.php | 58 +------------------ src/SourceImporter/SourceImporterGeoJson.php | 2 +- src/SourceManager.php | 2 +- src/SourceReader/SourceReaderGeoJson.php | 2 +- 6 files changed, 16 insertions(+), 106 deletions(-) diff --git a/src/Source/AbstractSource.php b/src/Source/AbstractSource.php index 4f655ed..2a555f9 100644 --- a/src/Source/AbstractSource.php +++ b/src/Source/AbstractSource.php @@ -4,40 +4,17 @@ /** * Abstract source. - * - * Extend this to easily create a source implemting the SourceInterface via constructor arguments. */ abstract readonly class AbstractSource implements SourceInterface { - public function __construct( - public string $id, - public string $title, - public string $description, - public string $publisher, - public string $contact, - public string $landingPage, - public string $accessUrl, - public string $mediaType, - public string $crs, - public string $model, - public string $contextUrl, - public string $updateFrequency, - public ?string $licence = null, - /** - * @var array - */ - public array $omittedFields = [], - ) { - } - public function key(): string { - return $this->id; + return $this->definition->id; } public function __toString(): string { - return sprintf('%s (%s)', $this->title, $this->id); + return sprintf('%s (%s)', $this->definition->title, $this->definition->id); } /** @@ -45,22 +22,7 @@ public function __toString(): string */ public function toArray(): array { - return [ - 'id' => $this->id, - 'title' => $this->title, - 'description' => $this->description, - 'publisher' => $this->publisher, - 'contact' => $this->contact, - 'landing_page' => $this->landingPage, - 'access_url' => $this->accessUrl, - 'media_type' => $this->mediaType, - 'crs' => $this->crs, - 'model' => $this->model, - 'context_url' => $this->contextUrl, - 'update_frequency' => $this->updateFrequency, - 'licence' => $this->licence, - 'omitted_fields' => $this->omittedFields, - ]; + return $this->definition->toArray(); } /** diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 1ccf26a..124d4a9 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -7,16 +7,19 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; +use App\Source\Definition; /** * Disabled parking bays in Aarhus Municipality. */ final readonly class HandicapParking extends AbstractSource { + public Definition $definition; + public function __construct( private Wgs84Transformer $transformer, ) { - parent::__construct( + $this->definition = new Definition( id: 'mtm_spatialmaps-handicap-parking', title: 'Handicapparkering, Aarhus Kommune', description: 'Disabled parking bays in Aarhus Municipality, with the number of reserved bays per location.', @@ -29,6 +32,7 @@ public function __construct( model: 'OnStreetParking', contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', updateFrequency: 'continuous', + licence: null, omittedFields: [ 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', @@ -64,8 +68,8 @@ public function createNgsiEntity(array $data): ?NgsiEntity } $entity = new NgsiEntity( - \sprintf('urn:ngsi-ld:%s:aarhus-handicap-%s', $this->model, $key), - $this->model + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-%s', $this->definition->model, $key), + $this->definition->model ); return $entity @@ -73,8 +77,8 @@ public function createNgsiEntity(array $data): ?NgsiEntity ->setProperty('description', trim((string) ($row['bemrk'] ?? ''))) ->setProperty('category', ['forDisabled']) ->setProperty('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) - ->setProperty('source', $this->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($this->crs, $geometry)); + ->setProperty('source', $this->definition->accessUrl) + ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); } /** diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php index e010081..11f750f 100644 --- a/src/Source/SourceInterface.php +++ b/src/Source/SourceInterface.php @@ -17,63 +17,7 @@ #[AutoconfigureTag('app.source')] interface SourceInterface extends \Stringable, \JsonSerializable { - public string $id { - get; - } - - public string $title { - get; - } - public string $description { - get; - } - public string $publisher { - get; - } - public string $contact { - get; - } - public string $landingPage { - get; - } - - // @todo access_url? What access? Isn't it just a URL? - public string $accessUrl { - get; - } - - public string $mediaType { - get; - } - - public string $crs { - get; - } - - public string $model { - get; - } - - public string $contextUrl { - get; - } - - public string $updateFrequency { - get; - } - - public ?string $licence { - get; - } - - // @todo What does this mean? - // Fields the feed carries that are not published. Recorded here because - // the source class shows what is mapped but cannot show what was left - // out, or why. - /** - * @var array - */ - public array $omittedFields { + public Definition $definition { get; } diff --git a/src/SourceImporter/SourceImporterGeoJson.php b/src/SourceImporter/SourceImporterGeoJson.php index af888b9..41a9dcc 100644 --- a/src/SourceImporter/SourceImporterGeoJson.php +++ b/src/SourceImporter/SourceImporterGeoJson.php @@ -46,7 +46,7 @@ public function import(SourceInterface $source): ImportResult { $this->info('Processing source {source}', ['source' => $source->__toString()]); - $contextUrls = array_merge([$source->contextUrl], $this->contextUrls); + $contextUrls = array_merge([$source->definition->contextUrl], $this->contextUrls); $payload = []; foreach ($this->read($source) as $entity) { $this->info('Building payload for {entity}', ['entity' => $entity->id()]); diff --git a/src/SourceManager.php b/src/SourceManager.php index 681ed07..8df03a1 100644 --- a/src/SourceManager.php +++ b/src/SourceManager.php @@ -37,7 +37,7 @@ public function getSources(): array if (!$source instanceof SourceInterface) { throw new \InvalidArgumentException(sprintf('Invalid source class: %s (must extend %s)', $source::class, AbstractSource::class)); } - $id = $source->id; + $id = $source->definition->id; if (isset($sources[$id])) { throw new \RuntimeException(sprintf('Duplicate source: %s (ID already used by %s)', $id, $sources[$id]::class)); } diff --git a/src/SourceReader/SourceReaderGeoJson.php b/src/SourceReader/SourceReaderGeoJson.php index 9593275..f3fb50d 100644 --- a/src/SourceReader/SourceReaderGeoJson.php +++ b/src/SourceReader/SourceReaderGeoJson.php @@ -20,7 +20,7 @@ public function __construct( public function read(SourceInterface $source): iterable { // @todo Add some proper exception handling/logging. - $data = $this->getData($source->accessUrl); + $data = $this->getData($source->definition->accessUrl); $features = $data['features']; From 71df0c6bcafaced8ca4fad81fd5a2a7563d95ab9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 11 Sep 2026 12:24:26 +0200 Subject: [PATCH 069/101] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8dae0..3518a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-12](https://github.com/itk-dev/enter/pull/12) + Add source definition as class * [PR-9](https://github.com/itk-dev/enter/pull/9) Refactored source import * [PR-7](https://github.com/itk-dev/enter/pull/7) From c04fb8f3889a9e6d80a4529904caafc45731b11f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 13:32:23 +0200 Subject: [PATCH 070/101] Added OSM handicap parking dataset --- config/sources.yaml | 46 ++++++ src/Source/Osm/HandicapParking.php | 226 +++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 src/Source/Osm/HandicapParking.php diff --git a/config/sources.yaml b/config/sources.yaml index bebb760..13f78a0 100644 --- a/config/sources.yaml +++ b/config/sources.yaml @@ -35,3 +35,49 @@ sources: oprettet_dato: 'Describes the register record.' rettet_dato: 'Describes the register record.' mi_style: 'MapInfo rendering style, empty throughout the export.' + + osm-handicap-parking: + title: 'Handicapparkering (OpenStreetMap), Aarhus Kommune' + description: >- + Disabled parking mapped in OpenStreetMap within Aarhus + Municipality: single reserved bays, and parking facilities that + state how many of their bays are reserved. Read via the Overpass + API. + publisher: 'OpenStreetMap contributors' + + # Community-maintained data; there is no single contact. + contact: ~ + landing_page: 'https://wiki.openstreetmap.org/wiki/Tag:parking_space%3Ddisabled' + + # The Overpass QL in the URL: within Aarhus Municipality (OSM relation + # 1784663), select every element tagged as a disabled parking space + # (parking_space=disabled) or as reserving bays for disabled parking + # (capacity:disabled, excluding "no" and "0"). + access_url: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B' + media_type: application/json + crs: 'EPSG:4326' + model: OnStreetParking + update_frequency: continuous + + # Republication must attribute "© OpenStreetMap contributors" and + # share under the same licence. + licence: 'https://opendatacommons.org/licenses/odbl/1-0/' + + # OpenStreetMap tagging is open-ended, so unlike a fixed-schema feed + # this cannot list everything an element may carry. These are the + # recurring tags in the current extract that are not published; + # coverage figures count records carrying the tag in the September + # 2026 extract. + omitted_fields: + amenity: 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.' + capacity: 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.' + parking: 'Facility siting (street_side, surface, underground); every record is published under the one model the manifest names.' + orientation: 'How bays lie relative to the road; the model has no counterpart.' + disabled: 'Access restriction on street-side parking; redundant with the category every entity is published with.' + access: 'Who may enter; mapping it onto permit attributes needs an interpretation the tag values do not support.' + 'fee:conditional': 'Time-qualified refinement of fee; the category values the plain fee tag maps onto carry no schedule.' + surface: 'Paving material; the model has no counterpart. 6% of records carry it.' + wheelchair: 'Step-free access to the place, not the parking capacity. 3% of records carry it.' + 'capacity:charging': 'Bays with charging points; a different subset than the reserved bays this data set publishes. 2% of records carry it.' + operator: 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.' + brand: 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.' diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php new file mode 100644 index 0000000..f8fec01 --- /dev/null +++ b/src/Source/Osm/HandicapParking.php @@ -0,0 +1,226 @@ +catalog->get(self::KEY); + + // Overpass wraps the matched OSM objects in an envelope with version + // and timestamp metadata; the records live under `elements`. + foreach ($this->reader->read($source->accessUrl)['elements'] ?? [] as $element) { + if (\is_array($element) && null !== $entity = $this->toEntity($element, $source)) { + yield $entity; + } + } + } + + /** + * @param array $element Overpass JSON element + */ + private function toEntity(array $element, SourceDescriptor $source): ?NgsiEntity + { + $type = $element['type'] ?? null; + $id = $element['id'] ?? null; + + // OSM ids are only unique per element type, so both are needed to + // address the same object again on the next import. + if (!\is_string($type) || !\is_int($id)) { + return null; + } + + $geometry = $this->geometry($element); + if (null === $geometry) { + return null; + } + + $tags = \is_array($element['tags'] ?? null) ? $element['tags'] : []; + + $entity = new NgsiEntity( + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-%s-%d', $source->model, $type, $id), + $source->model + ); + + return $entity + ->property('name', trim((string) ($tags['name'] ?? ''))) + ->property('description', trim((string) ($tags['description'] ?? ''))) + ->property('category', $this->category($tags)) + ->property('totalSpotNumber', $this->reservedBays($tags)) + ->property('source', $source->accessUrl) + ->geoProperty('location', $this->transformer->geometry($source->crs, $geometry)); + } + + /** + * Every record is disabled parking; the fee tag refines that with the + * model's charging categories. Only its two plain values map — an + * untagged or unrecognised value states nothing about charging rather + * than assuming free. + * + * @param array $tags + * + * @return list + */ + private function category(array $tags): array + { + return match ($tags['fee'] ?? null) { + 'yes' => ['forDisabled', 'feeCharged'], + 'no' => ['forDisabled', 'free'], + default => ['forDisabled'], + }; + } + + /** + * Number of reserved bays the record carries. + * + * capacity:disabled counts them directly whatever the record is. A single + * bay (parking_space=disabled) is reserved in its entirety, so its own + * capacity applies — one when untagged, per the tag's definition. A + * facility's plain capacity counts all its bays and is never used, and + * capacity:disabled=yes states that reserved bays exist without counting + * them, so nothing is published for it. + * + * @param array $tags + */ + private function reservedBays(array $tags): ?int + { + if (null !== $count = $this->count($tags, 'capacity:disabled')) { + return $count; + } + + if ('disabled' === ($tags['parking_space'] ?? null)) { + return $this->count($tags, 'capacity') ?? 1; + } + + return null; + } + + /** + * @param array $tags + */ + private function count(array $tags, string $tag): ?int + { + $value = $tags[$tag] ?? null; + + return \is_string($value) && ctype_digit($value) ? (int) $value : null; + } + + /** + * @param array $element + * + * @return array{type: string, coordinates: mixed}|null + */ + private function geometry(array $element): ?array + { + return match ($element['type'] ?? null) { + 'node' => $this->point($element['lon'] ?? null, $element['lat'] ?? null), + 'way' => $this->wayGeometry($element['geometry'] ?? null), + // The feed's output mode carries no member geometry for + // relations, only their bounding box, so the centre of that box + // is the best location available. + 'relation' => $this->boundsCentre($element['bounds'] ?? null), + default => null, + }; + } + + /** + * @return array{type: string, coordinates: array{float, float}}|null + */ + private function point(mixed $longitude, mixed $latitude): ?array + { + if (!is_numeric($longitude) || !is_numeric($latitude)) { + return null; + } + + return ['type' => 'Point', 'coordinates' => [(float) $longitude, (float) $latitude]]; + } + + /** + * @return array{type: string, coordinates: mixed}|null + */ + private function wayGeometry(mixed $vertices): ?array + { + if (!\is_array($vertices)) { + return null; + } + + $positions = []; + foreach ($vertices as $vertex) { + if (!\is_array($vertex) || !is_numeric($vertex['lon'] ?? null) || !is_numeric($vertex['lat'] ?? null)) { + return null; + } + + $positions[] = [(float) $vertex['lon'], (float) $vertex['lat']]; + } + + // A way that returns to its first vertex outlines an area — here a + // bay or a parking lot — so it becomes a Polygon ring rather than a + // line along its edge. Four positions are a ring's minimum: three + // corners plus the repeated first. + if (\count($positions) >= 4 && $positions[0] === $positions[array_key_last($positions)]) { + return ['type' => 'Polygon', 'coordinates' => [$positions]]; + } + + if (\count($positions) >= 2) { + return ['type' => 'LineString', 'coordinates' => $positions]; + } + + return null; + } + + /** + * @return array{type: string, coordinates: array{float, float}}|null + */ + private function boundsCentre(mixed $bounds): ?array + { + if (!\is_array($bounds)) { + return null; + } + + foreach (['minlon', 'minlat', 'maxlon', 'maxlat'] as $edge) { + if (!is_numeric($bounds[$edge] ?? null)) { + return null; + } + } + + return $this->point( + ((float) $bounds['minlon'] + (float) $bounds['maxlon']) / 2, + ((float) $bounds['minlat'] + (float) $bounds['maxlat']) / 2 + ); + } +} From b5fc1260ba44739467ecf65808d0fd707f062db5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 13:32:32 +0200 Subject: [PATCH 071/101] Added tests --- tests/Source/Osm/HandicapParkingTest.php | 339 +++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 tests/Source/Osm/HandicapParkingTest.php diff --git a/tests/Source/Osm/HandicapParkingTest.php b/tests/Source/Osm/HandicapParkingTest.php new file mode 100644 index 0000000..dfd3e73 --- /dev/null +++ b/tests/Source/Osm/HandicapParkingTest.php @@ -0,0 +1,339 @@ +> */ + private array $entities; + + protected function setUp(): void + { + $catalog = new SourceCatalog(\dirname(__DIR__, 3).'/config/sources.yaml'); + $this->source = $catalog->get(self::KEY); + + $client = new MockHttpClient(function (string $method, string $url): MockResponse { + $this->requestedUrl = $url; + + return new MockResponse(json_encode($this->feed(), \JSON_THROW_ON_ERROR)); + }); + + $source = new HandicapParking(new FeedReader($client), new Wgs84Transformer(), $catalog); + + $this->entities = array_map( + static fn (NgsiEntity $entity): array => $entity->toArray(['https://example.com/context.jsonld']), + iterator_to_array($source->entities(), false) + ); + } + + public function testItReadsTheFeedTheManifestPointsAt(): void + { + $this->assertSame($this->source->accessUrl, $this->requestedUrl); + } + + public function testItSkipsRecordsWithoutAnIdentifierOrGeometry(): void + { + // Nine elements, of which one has no id and one no coordinates. + $this->assertCount(7, $this->entities); + } + + public function testItAddressesEntitiesByOsmTypeAndId(): void + { + // OSM ids are only unique per element type, so the type is part of + // the identifier; the osm marker keeps it clear of other data sets' + // aarhus-handicap ids. + $this->assertSame( + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-node-3580886094', $this->source->model), + $this->entities[0]['id'] + ); + $this->assertSame( + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-way-384028175', $this->source->model), + $this->entities[2]['id'] + ); + $this->assertSame( + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-relation-17151325', $this->source->model), + $this->entities[4]['id'] + ); + } + + public function testItTakesTheTypeFromTheManifestModel(): void + { + foreach ($this->entities as $entity) { + $this->assertSame($this->source->model, $entity['type']); + } + } + + public function testItMarksEveryEntityAsDisabledParking(): void + { + foreach ($this->entities as $entity) { + $this->assertContains('forDisabled', $entity['category']['value']); + } + } + + public function testItRefinesTheCategoryFromTheFeeTag(): void + { + // fee=yes and fee=no map onto the model's feeCharged and free + // categories; a record without the tag states nothing about charging. + $this->assertSame(['forDisabled', 'feeCharged'], $this->entities[0]['category']['value']); + $this->assertSame(['forDisabled', 'free'], $this->entities[4]['category']['value']); + $this->assertSame(['forDisabled'], $this->entities[2]['category']['value']); + } + + public function testItIgnoresAFeeValueItDoesNotRecognise(): void + { + // fee=donation neither confirms a charge nor rules one out. + $this->assertSame(['forDisabled'], $this->entities[6]['category']['value']); + } + + public function testItReadsTheReservedBayCountFromCapacityDisabled(): void + { + $this->assertSame(4, $this->entities[0]['totalSpotNumber']['value']); + + // Q-Park SHIP has capacity 299; only its three reserved bays count. + $this->assertSame(3, $this->entities[1]['totalSpotNumber']['value']); + } + + public function testItCountsASingleBayByItsOwnCapacity(): void + { + // A parking_space=disabled record is reserved in its entirety, so its + // capacity is the reserved count. + $this->assertSame(1, $this->entities[2]['totalSpotNumber']['value']); + $this->assertSame(3, $this->entities[3]['totalSpotNumber']['value']); + } + + public function testItCountsOneBayWhenASingleBayCarriesNoCapacity(): void + { + $this->assertSame(1, $this->entities[6]['totalSpotNumber']['value']); + } + + public function testItOmitsTheBayCountWhenOnlyItsExistenceIsTagged(): void + { + // capacity:disabled=yes; the facility's total capacity of 36 counts + // every bay and must not stand in for the reserved ones. + $this->assertArrayNotHasKey('totalSpotNumber', $this->entities[5]); + } + + public function testItPublishesTheNameWhenOneIsMapped(): void + { + $this->assertSame('Q-Park SHIP', $this->entities[1]['name']['value']); + $this->assertArrayNotHasKey('name', $this->entities[0]); + } + + public function testItPublishesTheDescriptionWhenOneIsMapped(): void + { + $this->assertSame('Ved hovedindgangen', $this->entities[6]['description']['value']); + $this->assertArrayNotHasKey('description', $this->entities[0]); + } + + public function testItPublishesANodeAsAPoint(): void + { + $location = $this->entities[0]['location']; + + $this->assertSame('GeoProperty', $location['type']); + $this->assertSame('Point', $location['value']['type']); + + // The feed is already WGS84, so the coordinates pass through + // unchanged — in GeoJSON order, longitude first. + $this->assertSame([10.2141175, 56.1540563], $location['value']['coordinates']); + } + + public function testItPublishesAClosedWayAsAPolygon(): void + { + $geometry = $this->entities[2]['location']['value']; + + $this->assertSame('Polygon', $geometry['type']); + + $ring = $geometry['coordinates'][0]; + $this->assertCount(5, $ring); + $this->assertSame($ring[0], $ring[4]); + $this->assertSame([10.2101549, 56.1572442], $ring[0]); + } + + public function testItPublishesAnOpenWayAsALineString(): void + { + $geometry = $this->entities[3]['location']['value']; + + $this->assertSame('LineString', $geometry['type']); + $this->assertSame( + [[10.1061762, 56.1832145], [10.1061762, 56.1832532]], + $geometry['coordinates'] + ); + } + + public function testItPublishesARelationAtTheCentreOfItsBounds(): void + { + // The feed's output mode gives a relation no member geometry, only a + // bounding box, so its centre stands in for the location. + $geometry = $this->entities[4]['location']['value']; + + $this->assertSame('Point', $geometry['type']); + $this->assertEqualsWithDelta(10.24793075, $geometry['coordinates'][0], 1e-9); + $this->assertEqualsWithDelta(56.08875175, $geometry['coordinates'][1], 1e-9); + } + + public function testItRecordsTheManifestUrlAsTheEntitySource(): void + { + $this->assertSame($this->source->accessUrl, $this->entities[0]['source']['value']); + } + + /** + * The first five elements are records from the live feed — a facility + * node, a named facility, a closed bay way, a bay way and a relation — + * kept verbatim except the second way, whose geometry is cut to two + * vertices to exercise the open-way path. The rest are constructed for + * the untagged capacity default, capacity:disabled=yes, an unrecognised + * fee value and the two guards that discard a record. + * + * @return array + */ + private function feed(): array + { + return [ + 'version' => 0.6, + 'generator' => 'Overpass API 0.7.62.11 87bfad18', + 'osm3s' => [ + 'timestamp_osm_base' => '2026-09-07T09:01:36Z', + 'timestamp_areas_base' => '2026-09-06T23:47:02Z', + 'copyright' => 'The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.', + ], + 'elements' => [ + [ + 'type' => 'node', + 'id' => 3580886094, + 'lat' => 56.1540563, + 'lon' => 10.2141175, + 'tags' => [ + 'access' => 'yes', + 'amenity' => 'parking', + 'capacity:disabled' => '4', + 'fee' => 'yes', + 'parking' => 'surface', + ], + ], + [ + 'type' => 'node', + 'id' => 12368170867, + 'lat' => 56.1676256, + 'lon' => 10.2255202, + 'tags' => [ + 'amenity' => 'parking', + 'brand' => 'Q-Park', + 'brand:wikidata' => 'Q1127798', + 'capacity' => '299', + 'capacity:disabled' => '3', + 'fee' => 'yes', + 'layer' => '-1', + 'name' => 'Q-Park SHIP', + 'operator' => 'Q-Park', + 'operator:type' => 'private', + 'operator:wikidata' => 'Q1127798', + 'parking' => 'underground', + ], + ], + [ + 'type' => 'way', + 'id' => 384028175, + 'bounds' => ['minlat' => 56.1572328, 'minlon' => 10.2101549, 'maxlat' => 56.1572932, 'maxlon' => 10.2102509], + 'geometry' => [ + ['lat' => 56.1572442, 'lon' => 10.2101549], + ['lat' => 56.1572328, 'lon' => 10.2102254], + ['lat' => 56.1572819, 'lon' => 10.2102509], + ['lat' => 56.1572932, 'lon' => 10.2101805], + ['lat' => 56.1572442, 'lon' => 10.2101549], + ], + 'tags' => [ + 'amenity' => 'parking_space', + 'capacity' => '1', + 'parking_space' => 'disabled', + ], + ], + [ + 'type' => 'way', + 'id' => 1180544298, + 'bounds' => ['minlat' => 56.1832145, 'minlon' => 10.1061762, 'maxlat' => 56.1832532, 'maxlon' => 10.1063419], + 'geometry' => [ + ['lat' => 56.1832145, 'lon' => 10.1061762], + ['lat' => 56.1832532, 'lon' => 10.1061762], + ], + 'tags' => [ + 'amenity' => 'parking_space', + 'capacity' => '3', + 'parking_space' => 'disabled', + ], + ], + [ + 'type' => 'relation', + 'id' => 17151325, + 'bounds' => ['minlat' => 56.0886708, 'minlon' => 10.2477857, 'maxlat' => 56.0888327, 'maxlon' => 10.2480758], + 'tags' => [ + 'access' => 'yes', + 'amenity' => 'parking', + 'capacity' => '11', + 'capacity:disabled' => '1', + 'fee' => 'no', + 'orientation' => 'perpendicular', + 'parking' => 'street_side', + 'surface' => 'asphalt', + 'type' => 'multipolygon', + ], + ], + [ + 'type' => 'node', + 'id' => 101, + 'lat' => 56.15, + 'lon' => 10.21, + 'tags' => [ + 'amenity' => 'parking', + 'capacity' => '36', + 'capacity:disabled' => 'yes', + ], + ], + [ + 'type' => 'node', + 'id' => 102, + 'lat' => 56.16, + 'lon' => 10.22, + 'tags' => [ + 'amenity' => 'parking_space', + 'parking_space' => 'disabled', + 'description' => 'Ved hovedindgangen', + 'fee' => 'donation', + ], + ], + [ + 'type' => 'node', + 'lat' => 56.17, + 'lon' => 10.23, + 'tags' => ['parking_space' => 'disabled'], + ], + [ + 'type' => 'node', + 'id' => 103, + 'tags' => ['parking_space' => 'disabled'], + ], + ], + ]; + } +} From 8373cbf4ed75f725f9ca7a6d5463eff9f3288218 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 7 Sep 2026 13:33:34 +0200 Subject: [PATCH 072/101] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3518a49..d62a5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Refactored source import * [PR-7](https://github.com/itk-dev/enter/pull/7) Refactored source definition +* [#5](https://github.com/itk-dev/enter/pull/5) + * Added osm-handicap-parking. * [#3](https://github.com/itk-dev/enter/pull/3) * Import command that reads a geospatial feed, reprojects it to WGS84 and upserts it to an NGSI-LD broker. * A committed record per data set — feed, CRS, model, DCAT-AP metadata. From 12493fd462a9c968ea7f0aed553b0951314df92a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 8 Sep 2026 15:44:01 +0200 Subject: [PATCH 073/101] Adjust to changes from base branch --- config/sources.yaml | 1 + src/Source/Osm/HandicapParking.php | 24 ++++++++++++------------ tests/Source/Osm/HandicapParkingTest.php | 12 ++++++------ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/config/sources.yaml b/config/sources.yaml index 13f78a0..2aa3d26 100644 --- a/config/sources.yaml +++ b/config/sources.yaml @@ -57,6 +57,7 @@ sources: media_type: application/json crs: 'EPSG:4326' model: OnStreetParking + context_url: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld' update_frequency: continuous # Republication must attribute "© OpenStreetMap contributors" and diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index f8fec01..3ab4f6d 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -6,9 +6,9 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; -use App\Source\FeedReader; -use App\Source\SourceCatalog; -use App\Source\SourceDescriptor; +use App\Source\DataSourceReader; +use App\Source\Manifest\Catalog; +use App\Source\Manifest\Descriptor; use App\Source\SourceInterface; /** @@ -27,9 +27,9 @@ private const string KEY = 'osm-handicap-parking'; public function __construct( - private FeedReader $reader, + private DataSourceReader $reader, private Wgs84Transformer $transformer, - private SourceCatalog $catalog, + private Catalog $catalog, ) { } @@ -54,7 +54,7 @@ public function entities(): iterable /** * @param array $element Overpass JSON element */ - private function toEntity(array $element, SourceDescriptor $source): ?NgsiEntity + private function toEntity(array $element, Descriptor $source): ?NgsiEntity { $type = $element['type'] ?? null; $id = $element['id'] ?? null; @@ -78,12 +78,12 @@ private function toEntity(array $element, SourceDescriptor $source): ?NgsiEntity ); return $entity - ->property('name', trim((string) ($tags['name'] ?? ''))) - ->property('description', trim((string) ($tags['description'] ?? ''))) - ->property('category', $this->category($tags)) - ->property('totalSpotNumber', $this->reservedBays($tags)) - ->property('source', $source->accessUrl) - ->geoProperty('location', $this->transformer->geometry($source->crs, $geometry)); + ->setProperty('name', trim((string) ($tags['name'] ?? ''))) + ->setProperty('description', trim((string) ($tags['description'] ?? ''))) + ->setProperty('category', $this->category($tags)) + ->setProperty('totalSpotNumber', $this->reservedBays($tags)) + ->setProperty('source', $source->accessUrl) + ->geoProperty('location', $this->transformer->transformGeometry($source->crs, $geometry)); } /** diff --git a/tests/Source/Osm/HandicapParkingTest.php b/tests/Source/Osm/HandicapParkingTest.php index dfd3e73..e84df81 100644 --- a/tests/Source/Osm/HandicapParkingTest.php +++ b/tests/Source/Osm/HandicapParkingTest.php @@ -6,10 +6,10 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; -use App\Source\FeedReader; +use App\Source\DataSourceReader; +use App\Source\Manifest\Catalog; +use App\Source\Manifest\Descriptor; use App\Source\Osm\HandicapParking; -use App\Source\SourceCatalog; -use App\Source\SourceDescriptor; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; @@ -22,7 +22,7 @@ class HandicapParkingTest extends TestCase { private const string KEY = 'osm-handicap-parking'; - private SourceDescriptor $source; + private Descriptor $source; private string $requestedUrl; @@ -31,7 +31,7 @@ class HandicapParkingTest extends TestCase protected function setUp(): void { - $catalog = new SourceCatalog(\dirname(__DIR__, 3).'/config/sources.yaml'); + $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); $this->source = $catalog->get(self::KEY); $client = new MockHttpClient(function (string $method, string $url): MockResponse { @@ -40,7 +40,7 @@ protected function setUp(): void return new MockResponse(json_encode($this->feed(), \JSON_THROW_ON_ERROR)); }); - $source = new HandicapParking(new FeedReader($client), new Wgs84Transformer(), $catalog); + $source = new HandicapParking(new DataSourceReader($client), new Wgs84Transformer(), $catalog); $this->entities = array_map( static fn (NgsiEntity $entity): array => $entity->toArray(['https://example.com/context.jsonld']), From 98b27c2ed1566dbb32929436639353a028b08cdc Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 11 Sep 2026 14:34:10 +0200 Subject: [PATCH 074/101] Removed entry from sources.yaml --- config/sources.yaml | 47 --------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/config/sources.yaml b/config/sources.yaml index 2aa3d26..bebb760 100644 --- a/config/sources.yaml +++ b/config/sources.yaml @@ -35,50 +35,3 @@ sources: oprettet_dato: 'Describes the register record.' rettet_dato: 'Describes the register record.' mi_style: 'MapInfo rendering style, empty throughout the export.' - - osm-handicap-parking: - title: 'Handicapparkering (OpenStreetMap), Aarhus Kommune' - description: >- - Disabled parking mapped in OpenStreetMap within Aarhus - Municipality: single reserved bays, and parking facilities that - state how many of their bays are reserved. Read via the Overpass - API. - publisher: 'OpenStreetMap contributors' - - # Community-maintained data; there is no single contact. - contact: ~ - landing_page: 'https://wiki.openstreetmap.org/wiki/Tag:parking_space%3Ddisabled' - - # The Overpass QL in the URL: within Aarhus Municipality (OSM relation - # 1784663), select every element tagged as a disabled parking space - # (parking_space=disabled) or as reserving bays for disabled parking - # (capacity:disabled, excluding "no" and "0"). - access_url: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B' - media_type: application/json - crs: 'EPSG:4326' - model: OnStreetParking - context_url: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld' - update_frequency: continuous - - # Republication must attribute "© OpenStreetMap contributors" and - # share under the same licence. - licence: 'https://opendatacommons.org/licenses/odbl/1-0/' - - # OpenStreetMap tagging is open-ended, so unlike a fixed-schema feed - # this cannot list everything an element may carry. These are the - # recurring tags in the current extract that are not published; - # coverage figures count records carrying the tag in the September - # 2026 extract. - omitted_fields: - amenity: 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.' - capacity: 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.' - parking: 'Facility siting (street_side, surface, underground); every record is published under the one model the manifest names.' - orientation: 'How bays lie relative to the road; the model has no counterpart.' - disabled: 'Access restriction on street-side parking; redundant with the category every entity is published with.' - access: 'Who may enter; mapping it onto permit attributes needs an interpretation the tag values do not support.' - 'fee:conditional': 'Time-qualified refinement of fee; the category values the plain fee tag maps onto carry no schedule.' - surface: 'Paving material; the model has no counterpart. 6% of records carry it.' - wheelchair: 'Step-free access to the place, not the parking capacity. 3% of records carry it.' - 'capacity:charging': 'Bays with charging points; a different subset than the reserved bays this data set publishes. 2% of records carry it.' - operator: 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.' - brand: 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.' From 1c63fe372785965533a09768992b0f834b4e49c8 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 11 Sep 2026 14:34:37 +0200 Subject: [PATCH 075/101] Adjusted new adapter to fit new environment --- src/Source/Osm/HandicapParking.php | 99 +++++--- tests/Source/Osm/HandicapParkingTest.php | 279 ++++++++++------------- 2 files changed, 187 insertions(+), 191 deletions(-) diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index 3ab4f6d..73fc627 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -6,10 +6,8 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; -use App\Source\DataSourceReader; -use App\Source\Manifest\Catalog; -use App\Source\Manifest\Descriptor; -use App\Source\SourceInterface; +use App\Source\AbstractSource; +use App\Source\Definition; /** * Disabled parking in Aarhus Municipality, as mapped in OpenStreetMap. @@ -19,45 +17,72 @@ * (capacity:disabled). Both are published as one entity with the number of * reserved bays, so the distinction only matters for how that number is read. * - * @see config/sources.yaml * @see https://github.com/smart-data-models/dataModel.Parking/tree/master/OnStreetParking */ -final readonly class HandicapParking implements SourceInterface +final readonly class HandicapParking extends AbstractSource { - private const string KEY = 'osm-handicap-parking'; + public Definition $definition; public function __construct( - private DataSourceReader $reader, private Wgs84Transformer $transformer, - private Catalog $catalog, ) { - } - - public function key(): string - { - return self::KEY; - } - - public function entities(): iterable - { - $source = $this->catalog->get(self::KEY); - - // Overpass wraps the matched OSM objects in an envelope with version - // and timestamp metadata; the records live under `elements`. - foreach ($this->reader->read($source->accessUrl)['elements'] ?? [] as $element) { - if (\is_array($element) && null !== $entity = $this->toEntity($element, $source)) { - yield $entity; - } - } + $this->definition = new Definition( + id: 'osm-handicap-parking', + title: 'Handicapparkering (OpenStreetMap), Aarhus Kommune', + description: 'Disabled parking mapped in OpenStreetMap within Aarhus Municipality: single reserved bays, and parking facilities that state how many of their bays are reserved. Read via the Overpass API.', + publisher: 'OpenStreetMap contributors', + + // Community-maintained data, so no one owner answers for it; the + // forum is where a question about a record is raised. + contact: 'https://community.openstreetmap.org/', + landingPage: 'https://wiki.openstreetmap.org/wiki/Tag:parking_space%3Ddisabled', + + // The Overpass QL in the URL: within Aarhus Municipality (OSM + // relation 1784663), select every element tagged as a disabled + // parking space (parking_space=disabled) or as reserving bays for + // disabled parking (capacity:disabled, excluding "no" and "0"). + accessUrl: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', + mediaType: 'application/json', + crs: 'EPSG:4326', + model: 'OnStreetParking', + contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', + updateFrequency: 'continuous', + + // Republication must attribute "© OpenStreetMap contributors" and + // share under the same licence. + licence: 'https://opendatacommons.org/licenses/odbl/1-0/', + + // OpenStreetMap tagging is open-ended, so unlike a fixed-schema + // feed this cannot list everything an element may carry. These are + // the recurring tags in the current extract that are not + // published; coverage figures count records carrying the tag in + // the September 2026 extract. + omittedFields: [ + 'amenity' => 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', + 'capacity' => 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.', + 'parking' => 'Facility siting (street_side, surface, underground); every record is published under the one model this source names.', + 'orientation' => 'How bays lie relative to the road; the model has no counterpart.', + 'disabled' => 'Access restriction on street-side parking; redundant with the category every entity is published with.', + 'access' => 'Who may enter; mapping it onto permit attributes needs an interpretation the tag values do not support.', + 'fee:conditional' => 'Time-qualified refinement of fee; the category values the plain fee tag maps onto carry no schedule.', + 'surface' => 'Paving material; the model has no counterpart. 6% of records carry it.', + 'wheelchair' => 'Step-free access to the place, not the parking capacity. 3% of records carry it.', + 'capacity:charging' => 'Bays with charging points; a different subset than the reserved bays this data set publishes. 2% of records carry it.', + 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', + 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', + ], + ); } /** - * @param array $element Overpass JSON element + * Maps one feed record onto an NgsiEntity. + * + * @param array $data Overpass JSON element */ - private function toEntity(array $element, Descriptor $source): ?NgsiEntity + public function createNgsiEntity(array $data): ?NgsiEntity { - $type = $element['type'] ?? null; - $id = $element['id'] ?? null; + $type = $data['type'] ?? null; + $id = $data['id'] ?? null; // OSM ids are only unique per element type, so both are needed to // address the same object again on the next import. @@ -65,16 +90,16 @@ private function toEntity(array $element, Descriptor $source): ?NgsiEntity return null; } - $geometry = $this->geometry($element); + $geometry = $this->geometry($data); if (null === $geometry) { return null; } - $tags = \is_array($element['tags'] ?? null) ? $element['tags'] : []; + $tags = \is_array($data['tags'] ?? null) ? $data['tags'] : []; $entity = new NgsiEntity( - \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-%s-%d', $source->model, $type, $id), - $source->model + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-%s-%d', $this->definition->model, $type, $id), + $this->definition->model ); return $entity @@ -82,8 +107,8 @@ private function toEntity(array $element, Descriptor $source): ?NgsiEntity ->setProperty('description', trim((string) ($tags['description'] ?? ''))) ->setProperty('category', $this->category($tags)) ->setProperty('totalSpotNumber', $this->reservedBays($tags)) - ->setProperty('source', $source->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($source->crs, $geometry)); + ->setProperty('source', $this->definition->accessUrl) + ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); } /** diff --git a/tests/Source/Osm/HandicapParkingTest.php b/tests/Source/Osm/HandicapParkingTest.php index e84df81..02f8d5d 100644 --- a/tests/Source/Osm/HandicapParkingTest.php +++ b/tests/Source/Osm/HandicapParkingTest.php @@ -6,51 +6,31 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; -use App\Source\DataSourceReader; -use App\Source\Manifest\Catalog; -use App\Source\Manifest\Descriptor; use App\Source\Osm\HandicapParking; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpClient\MockHttpClient; -use Symfony\Component\HttpClient\Response\MockResponse; /** - * Runs against the manifest the project ships, not a fixture of one, so the - * entry this source depends on is covered too. Only the feed is mocked. + * Covers the mapping only. Reading the feed is the reader's job and is + * covered by App\Tests\SourceReader\SourceReaderOverpassTest. */ class HandicapParkingTest extends TestCase { - private const string KEY = 'osm-handicap-parking'; - - private Descriptor $source; - - private string $requestedUrl; + private HandicapParking $source; /** @var list> */ private array $entities; protected function setUp(): void { - $catalog = new Catalog(\dirname(__DIR__, 3).'/config/sources.yaml'); - $this->source = $catalog->get(self::KEY); - - $client = new MockHttpClient(function (string $method, string $url): MockResponse { - $this->requestedUrl = $url; - - return new MockResponse(json_encode($this->feed(), \JSON_THROW_ON_ERROR)); - }); - - $source = new HandicapParking(new DataSourceReader($client), new Wgs84Transformer(), $catalog); - - $this->entities = array_map( - static fn (NgsiEntity $entity): array => $entity->toArray(['https://example.com/context.jsonld']), - iterator_to_array($source->entities(), false) - ); - } - - public function testItReadsTheFeedTheManifestPointsAt(): void - { - $this->assertSame($this->source->accessUrl, $this->requestedUrl); + $this->source = new HandicapParking(new Wgs84Transformer()); + + $this->entities = array_values(array_map( + static fn (NgsiEntity $entity): array => $entity->toPayload(['https://example.com/context.jsonld']), + array_filter(array_map( + $this->source->createNgsiEntity(...), + $this->elements() + )) + )); } public function testItSkipsRecordsWithoutAnIdentifierOrGeometry(): void @@ -65,23 +45,23 @@ public function testItAddressesEntitiesByOsmTypeAndId(): void // the identifier; the osm marker keeps it clear of other data sets' // aarhus-handicap ids. $this->assertSame( - \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-node-3580886094', $this->source->model), + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-node-3580886094', $this->source->definition->model), $this->entities[0]['id'] ); $this->assertSame( - \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-way-384028175', $this->source->model), + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-way-384028175', $this->source->definition->model), $this->entities[2]['id'] ); $this->assertSame( - \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-relation-17151325', $this->source->model), + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-relation-17151325', $this->source->definition->model), $this->entities[4]['id'] ); } - public function testItTakesTheTypeFromTheManifestModel(): void + public function testItTakesTheTypeFromTheSourceModel(): void { foreach ($this->entities as $entity) { - $this->assertSame($this->source->model, $entity['type']); + $this->assertSame($this->source->definition->model, $entity['type']); } } @@ -193,9 +173,9 @@ public function testItPublishesARelationAtTheCentreOfItsBounds(): void $this->assertEqualsWithDelta(56.08875175, $geometry['coordinates'][1], 1e-9); } - public function testItRecordsTheManifestUrlAsTheEntitySource(): void + public function testItRecordsTheAccessUrlAsTheEntitySource(): void { - $this->assertSame($this->source->accessUrl, $this->entities[0]['source']['value']); + $this->assertSame($this->source->definition->accessUrl, $this->entities[0]['source']['value']); } /** @@ -206,134 +186,125 @@ public function testItRecordsTheManifestUrlAsTheEntitySource(): void * the untagged capacity default, capacity:disabled=yes, an unrecognised * fee value and the two guards that discard a record. * - * @return array + * @return list> */ - private function feed(): array + private function elements(): array { return [ - 'version' => 0.6, - 'generator' => 'Overpass API 0.7.62.11 87bfad18', - 'osm3s' => [ - 'timestamp_osm_base' => '2026-09-07T09:01:36Z', - 'timestamp_areas_base' => '2026-09-06T23:47:02Z', - 'copyright' => 'The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.', - ], - 'elements' => [ - [ - 'type' => 'node', - 'id' => 3580886094, - 'lat' => 56.1540563, - 'lon' => 10.2141175, - 'tags' => [ - 'access' => 'yes', - 'amenity' => 'parking', - 'capacity:disabled' => '4', - 'fee' => 'yes', - 'parking' => 'surface', - ], + [ + 'type' => 'node', + 'id' => 3580886094, + 'lat' => 56.1540563, + 'lon' => 10.2141175, + 'tags' => [ + 'access' => 'yes', + 'amenity' => 'parking', + 'capacity:disabled' => '4', + 'fee' => 'yes', + 'parking' => 'surface', ], - [ - 'type' => 'node', - 'id' => 12368170867, - 'lat' => 56.1676256, - 'lon' => 10.2255202, - 'tags' => [ - 'amenity' => 'parking', - 'brand' => 'Q-Park', - 'brand:wikidata' => 'Q1127798', - 'capacity' => '299', - 'capacity:disabled' => '3', - 'fee' => 'yes', - 'layer' => '-1', - 'name' => 'Q-Park SHIP', - 'operator' => 'Q-Park', - 'operator:type' => 'private', - 'operator:wikidata' => 'Q1127798', - 'parking' => 'underground', - ], + ], + [ + 'type' => 'node', + 'id' => 12368170867, + 'lat' => 56.1676256, + 'lon' => 10.2255202, + 'tags' => [ + 'amenity' => 'parking', + 'brand' => 'Q-Park', + 'brand:wikidata' => 'Q1127798', + 'capacity' => '299', + 'capacity:disabled' => '3', + 'fee' => 'yes', + 'layer' => '-1', + 'name' => 'Q-Park SHIP', + 'operator' => 'Q-Park', + 'operator:type' => 'private', + 'operator:wikidata' => 'Q1127798', + 'parking' => 'underground', ], - [ - 'type' => 'way', - 'id' => 384028175, - 'bounds' => ['minlat' => 56.1572328, 'minlon' => 10.2101549, 'maxlat' => 56.1572932, 'maxlon' => 10.2102509], - 'geometry' => [ - ['lat' => 56.1572442, 'lon' => 10.2101549], - ['lat' => 56.1572328, 'lon' => 10.2102254], - ['lat' => 56.1572819, 'lon' => 10.2102509], - ['lat' => 56.1572932, 'lon' => 10.2101805], - ['lat' => 56.1572442, 'lon' => 10.2101549], - ], - 'tags' => [ - 'amenity' => 'parking_space', - 'capacity' => '1', - 'parking_space' => 'disabled', - ], + ], + [ + 'type' => 'way', + 'id' => 384028175, + 'bounds' => ['minlat' => 56.1572328, 'minlon' => 10.2101549, 'maxlat' => 56.1572932, 'maxlon' => 10.2102509], + 'geometry' => [ + ['lat' => 56.1572442, 'lon' => 10.2101549], + ['lat' => 56.1572328, 'lon' => 10.2102254], + ['lat' => 56.1572819, 'lon' => 10.2102509], + ['lat' => 56.1572932, 'lon' => 10.2101805], + ['lat' => 56.1572442, 'lon' => 10.2101549], ], - [ - 'type' => 'way', - 'id' => 1180544298, - 'bounds' => ['minlat' => 56.1832145, 'minlon' => 10.1061762, 'maxlat' => 56.1832532, 'maxlon' => 10.1063419], - 'geometry' => [ - ['lat' => 56.1832145, 'lon' => 10.1061762], - ['lat' => 56.1832532, 'lon' => 10.1061762], - ], - 'tags' => [ - 'amenity' => 'parking_space', - 'capacity' => '3', - 'parking_space' => 'disabled', - ], + 'tags' => [ + 'amenity' => 'parking_space', + 'capacity' => '1', + 'parking_space' => 'disabled', ], - [ - 'type' => 'relation', - 'id' => 17151325, - 'bounds' => ['minlat' => 56.0886708, 'minlon' => 10.2477857, 'maxlat' => 56.0888327, 'maxlon' => 10.2480758], - 'tags' => [ - 'access' => 'yes', - 'amenity' => 'parking', - 'capacity' => '11', - 'capacity:disabled' => '1', - 'fee' => 'no', - 'orientation' => 'perpendicular', - 'parking' => 'street_side', - 'surface' => 'asphalt', - 'type' => 'multipolygon', - ], + ], + [ + 'type' => 'way', + 'id' => 1180544298, + 'bounds' => ['minlat' => 56.1832145, 'minlon' => 10.1061762, 'maxlat' => 56.1832532, 'maxlon' => 10.1063419], + 'geometry' => [ + ['lat' => 56.1832145, 'lon' => 10.1061762], + ['lat' => 56.1832532, 'lon' => 10.1061762], ], - [ - 'type' => 'node', - 'id' => 101, - 'lat' => 56.15, - 'lon' => 10.21, - 'tags' => [ - 'amenity' => 'parking', - 'capacity' => '36', - 'capacity:disabled' => 'yes', - ], + 'tags' => [ + 'amenity' => 'parking_space', + 'capacity' => '3', + 'parking_space' => 'disabled', ], - [ - 'type' => 'node', - 'id' => 102, - 'lat' => 56.16, - 'lon' => 10.22, - 'tags' => [ - 'amenity' => 'parking_space', - 'parking_space' => 'disabled', - 'description' => 'Ved hovedindgangen', - 'fee' => 'donation', - ], + ], + [ + 'type' => 'relation', + 'id' => 17151325, + 'bounds' => ['minlat' => 56.0886708, 'minlon' => 10.2477857, 'maxlat' => 56.0888327, 'maxlon' => 10.2480758], + 'tags' => [ + 'access' => 'yes', + 'amenity' => 'parking', + 'capacity' => '11', + 'capacity:disabled' => '1', + 'fee' => 'no', + 'orientation' => 'perpendicular', + 'parking' => 'street_side', + 'surface' => 'asphalt', + 'type' => 'multipolygon', ], - [ - 'type' => 'node', - 'lat' => 56.17, - 'lon' => 10.23, - 'tags' => ['parking_space' => 'disabled'], + ], + [ + 'type' => 'node', + 'id' => 101, + 'lat' => 56.15, + 'lon' => 10.21, + 'tags' => [ + 'amenity' => 'parking', + 'capacity' => '36', + 'capacity:disabled' => 'yes', ], - [ - 'type' => 'node', - 'id' => 103, - 'tags' => ['parking_space' => 'disabled'], + ], + [ + 'type' => 'node', + 'id' => 102, + 'lat' => 56.16, + 'lon' => 10.22, + 'tags' => [ + 'amenity' => 'parking_space', + 'parking_space' => 'disabled', + 'description' => 'Ved hovedindgangen', + 'fee' => 'donation', ], ], + [ + 'type' => 'node', + 'lat' => 56.17, + 'lon' => 10.23, + 'tags' => ['parking_space' => 'disabled'], + ], + [ + 'type' => 'node', + 'id' => 103, + 'tags' => ['parking_space' => 'disabled'], + ], ]; } } From 48371370906c6f9a82d95ac86ff03de6e4a137f2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 11 Sep 2026 14:38:17 +0200 Subject: [PATCH 076/101] Adjusted comments --- src/Source/Osm/HandicapParking.php | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index 73fc627..0555d49 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -10,14 +10,7 @@ use App\Source\Definition; /** - * Disabled parking in Aarhus Municipality, as mapped in OpenStreetMap. - * - * The feed carries two kinds of record: single reserved bays - * (parking_space=disabled) and parking facilities that reserve bays - * (capacity:disabled). Both are published as one entity with the number of - * reserved bays, so the distinction only matters for how that number is read. - * - * @see https://github.com/smart-data-models/dataModel.Parking/tree/master/OnStreetParking + * Disabled parking bays in Aarhus Municipality. */ final readonly class HandicapParking extends AbstractSource { @@ -29,11 +22,8 @@ public function __construct( $this->definition = new Definition( id: 'osm-handicap-parking', title: 'Handicapparkering (OpenStreetMap), Aarhus Kommune', - description: 'Disabled parking mapped in OpenStreetMap within Aarhus Municipality: single reserved bays, and parking facilities that state how many of their bays are reserved. Read via the Overpass API.', + description: 'Disabled parking mapped in OpenStreetMap within Aarhus Municipality: single reserved bays, and parking facilities that state how many of their bays are reserved.', publisher: 'OpenStreetMap contributors', - - // Community-maintained data, so no one owner answers for it; the - // forum is where a question about a record is raised. contact: 'https://community.openstreetmap.org/', landingPage: 'https://wiki.openstreetmap.org/wiki/Tag:parking_space%3Ddisabled', @@ -47,16 +37,8 @@ public function __construct( model: 'OnStreetParking', contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', updateFrequency: 'continuous', - - // Republication must attribute "© OpenStreetMap contributors" and - // share under the same licence. licence: 'https://opendatacommons.org/licenses/odbl/1-0/', - // OpenStreetMap tagging is open-ended, so unlike a fixed-schema - // feed this cannot list everything an element may carry. These are - // the recurring tags in the current extract that are not - // published; coverage figures count records carrying the tag in - // the September 2026 extract. omittedFields: [ 'amenity' => 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', 'capacity' => 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.', From 599148ebc06cd87a1d216aedf2a4ea4ac59c947c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 14 Sep 2026 12:30:24 +0200 Subject: [PATCH 077/101] Moved source metadata into an #[AsDataSource] attribute --- src/Source/AbstractSource.php | 12 ++- src/Source/AsDataSource.php | 17 ++++ src/Source/Definition.php | 23 +++++- src/Source/MtmSpatialMaps/HandicapParking.php | 60 +++++++------- src/Source/Osm/HandicapParking.php | 78 +++++++++---------- 5 files changed, 118 insertions(+), 72 deletions(-) create mode 100644 src/Source/AsDataSource.php diff --git a/src/Source/AbstractSource.php b/src/Source/AbstractSource.php index 2a555f9..168ac5f 100644 --- a/src/Source/AbstractSource.php +++ b/src/Source/AbstractSource.php @@ -5,8 +5,18 @@ /** * Abstract source. */ -abstract readonly class AbstractSource implements SourceInterface +abstract class AbstractSource implements SourceInterface { + /** + * Read from the #[AsDataSource] attribute on the concrete source. + */ + public Definition $definition { + /** + * @throws \ReflectionException + */ + get => Definition::of(static::class); + } + public function key(): string { return $this->definition->id; diff --git a/src/Source/AsDataSource.php b/src/Source/AsDataSource.php new file mode 100644 index 0000000..96f71f2 --- /dev/null +++ b/src/Source/AsDataSource.php @@ -0,0 +1,17 @@ + $omittedFields @@ -32,6 +32,25 @@ public function __construct( ) { } + /** + * The definition a source class declares. + * + * Reading it takes no instance, so the metadata of every data set is + * available without building the sources and their collaborators. + * + * @param class-string $class + * + * @throws \ReflectionException + */ + public static function of(string $class): self + { + $reflection = new \ReflectionClass($class); + $attribute = $reflection->getAttributes(AsDataSource::class)[0] + ?? throw new \LogicException(sprintf('Source %s declares no #[%s] attribute.', $class, AsDataSource::class)); + + return $attribute->newInstance(); + } + /** * @return array */ diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 124d4a9..f861dda 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -7,42 +7,44 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; -use App\Source\Definition; +use App\Source\AsDataSource; /** * Disabled parking bays in Aarhus Municipality. */ -final readonly class HandicapParking extends AbstractSource -{ - public Definition $definition; +#[AsDataSource( + id: 'mtm_spatialmaps-handicap-parking', + title: 'Handicapparkering, Aarhus Kommune', + description: 'Disabled parking bays in Aarhus Municipality, with the number of reserved bays per location.', + publisher: 'Aarhus Kommune', + contact: 'ppg@aarhus.dk', + landingPage: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune', + accessUrl: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', + mediaType: 'application/geo+json', + crs: 'EPSG:25832', + model: 'OnStreetParking', + contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', + updateFrequency: 'continuous', + + // The portal states no licence for this data set. DCAT-AP requires one, so + // it has to be settled with the data owner before the catalogue can be + // registered anywhere. + licence: null, + omittedFields: [ + 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', + 'oprettet_af' => 'Directory username of the municipal employee who created the record.', + 'rettet_af' => 'Directory username of the municipal employee who last edited the record.', + 'oprettet_dato' => 'Describes the register record.', + 'rettet_dato' => 'Describes the register record.', + 'mi_style' => 'MapInfo rendering style, empty throughout the export.', + ], +)] +final class HandicapParking extends AbstractSource +{ public function __construct( - private Wgs84Transformer $transformer, + private readonly Wgs84Transformer $transformer, ) { - $this->definition = new Definition( - id: 'mtm_spatialmaps-handicap-parking', - title: 'Handicapparkering, Aarhus Kommune', - description: 'Disabled parking bays in Aarhus Municipality, with the number of reserved bays per location.', - publisher: 'Aarhus Kommune', - contact: 'ppg@aarhus.dk', - landingPage: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune', - accessUrl: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', - mediaType: 'application/geo+json', - crs: 'EPSG:25832', - model: 'OnStreetParking', - contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', - updateFrequency: 'continuous', - licence: null, - - omittedFields: [ - 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', - 'oprettet_af' => 'Directory username of the municipal employee who created the record.', - 'rettet_af' => 'Directory username of the municipal employee who last edited the record.', - 'oprettet_dato' => 'Describes the register record.', - 'rettet_dato' => 'Describes the register record.', - 'mi_style' => 'MapInfo rendering style, empty throughout the export.', - ], - ); } /** diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index 0555d49..8af9f0f 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -7,53 +7,51 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; -use App\Source\Definition; +use App\Source\AsDataSource; /** * Disabled parking bays in Aarhus Municipality. */ -final readonly class HandicapParking extends AbstractSource +#[AsDataSource( + id: 'osm-handicap-parking', + title: 'Handicapparkering (OpenStreetMap), Aarhus Kommune', + description: 'Disabled parking mapped in OpenStreetMap within Aarhus Municipality: single reserved bays, and parking facilities that state how many of their bays are reserved.', + publisher: 'OpenStreetMap contributors', + contact: 'https://community.openstreetmap.org/', + landingPage: 'https://wiki.openstreetmap.org/wiki/Tag:parking_space%3Ddisabled', + + // The Overpass QL in the URL: within Aarhus Municipality (OSM + // relation 1784663), select every element tagged as a disabled + // parking space (parking_space=disabled) or as reserving bays for + // disabled parking (capacity:disabled, excluding "no" and "0"). + accessUrl: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', + mediaType: 'application/json', + crs: 'EPSG:4326', + model: 'OnStreetParking', + contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', + updateFrequency: 'continuous', + licence: 'https://opendatacommons.org/licenses/odbl/1-0/', + + omittedFields: [ + 'amenity' => 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', + 'capacity' => 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.', + 'parking' => 'Facility siting (street_side, surface, underground); every record is published under the one model this source names.', + 'orientation' => 'How bays lie relative to the road; the model has no counterpart.', + 'disabled' => 'Access restriction on street-side parking; redundant with the category every entity is published with.', + 'access' => 'Who may enter; mapping it onto permit attributes needs an interpretation the tag values do not support.', + 'fee:conditional' => 'Time-qualified refinement of fee; the category values the plain fee tag maps onto carry no schedule.', + 'surface' => 'Paving material; the model has no counterpart. 6% of records carry it.', + 'wheelchair' => 'Step-free access to the place, not the parking capacity. 3% of records carry it.', + 'capacity:charging' => 'Bays with charging points; a different subset than the reserved bays this data set publishes. 2% of records carry it.', + 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', + 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', + ], +)] +final class HandicapParking extends AbstractSource { - public Definition $definition; - public function __construct( - private Wgs84Transformer $transformer, + private readonly Wgs84Transformer $transformer, ) { - $this->definition = new Definition( - id: 'osm-handicap-parking', - title: 'Handicapparkering (OpenStreetMap), Aarhus Kommune', - description: 'Disabled parking mapped in OpenStreetMap within Aarhus Municipality: single reserved bays, and parking facilities that state how many of their bays are reserved.', - publisher: 'OpenStreetMap contributors', - contact: 'https://community.openstreetmap.org/', - landingPage: 'https://wiki.openstreetmap.org/wiki/Tag:parking_space%3Ddisabled', - - // The Overpass QL in the URL: within Aarhus Municipality (OSM - // relation 1784663), select every element tagged as a disabled - // parking space (parking_space=disabled) or as reserving bays for - // disabled parking (capacity:disabled, excluding "no" and "0"). - accessUrl: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', - mediaType: 'application/json', - crs: 'EPSG:4326', - model: 'OnStreetParking', - contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', - updateFrequency: 'continuous', - licence: 'https://opendatacommons.org/licenses/odbl/1-0/', - - omittedFields: [ - 'amenity' => 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', - 'capacity' => 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.', - 'parking' => 'Facility siting (street_side, surface, underground); every record is published under the one model this source names.', - 'orientation' => 'How bays lie relative to the road; the model has no counterpart.', - 'disabled' => 'Access restriction on street-side parking; redundant with the category every entity is published with.', - 'access' => 'Who may enter; mapping it onto permit attributes needs an interpretation the tag values do not support.', - 'fee:conditional' => 'Time-qualified refinement of fee; the category values the plain fee tag maps onto carry no schedule.', - 'surface' => 'Paving material; the model has no counterpart. 6% of records carry it.', - 'wheelchair' => 'Step-free access to the place, not the parking capacity. 3% of records carry it.', - 'capacity:charging' => 'Bays with charging points; a different subset than the reserved bays this data set publishes. 2% of records carry it.', - 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', - 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', - ], - ); } /** From bb7561119044adb23eeb3b2549c61d1565322c9f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 14 Sep 2026 12:30:32 +0200 Subject: [PATCH 078/101] Removed unused source manifest --- config/sources.yaml | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 config/sources.yaml diff --git a/config/sources.yaml b/config/sources.yaml deleted file mode 100644 index bebb760..0000000 --- a/config/sources.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# The data sets this application publishes, one entry per source key. -# -# Field names follow DCAT-AP, the metadata profile European data portals -# harvest, so registering a data set is a translation of its entry rather than -# a new survey. See docs/adr/007-source-manifest.md. - -sources: - mtm_spatialmaps-handicap-parking: - title: 'Handicapparkering, Aarhus Kommune' - description: >- - Disabled parking bays in Aarhus Municipality, with the number of - reserved bays per location. - publisher: 'Aarhus Kommune' - contact: ppg@aarhus.dk - landing_page: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune' - access_url: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap' - media_type: application/geo+json - crs: 'EPSG:25832' - model: OnStreetParking - context_url: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld' - update_frequency: continuous - - # The portal states no licence for this data set. DCAT-AP requires - # one, so it has to be settled with the data owner before the - # catalogue can be registered anywhere. - licence: ~ - - # Fields the feed carries that are not published. Recorded here - # because the source class shows what is mapped but cannot show what - # was left out, or why. - omitted_fields: - ident: 'Single-letter code; its meaning is not documented and not confirmed by the data owner.' - oprettet_af: 'Directory username of the municipal employee who created the record.' - rettet_af: 'Directory username of the municipal employee who last edited the record.' - oprettet_dato: 'Describes the register record.' - rettet_dato: 'Describes the register record.' - mi_style: 'MapInfo rendering style, empty throughout the export.' From 59473dc088c441c28f1b5c561c42b6abe4eaae30 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 14 Sep 2026 12:30:32 +0200 Subject: [PATCH 079/101] Added revision note to ADR 007 --- docs/adr/007-source-manifest.md | 20 ++++++++++++++++++++ docs/adr/README.md | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/adr/007-source-manifest.md b/docs/adr/007-source-manifest.md index 8bb90c3..7bd7f8f 100644 --- a/docs/adr/007-source-manifest.md +++ b/docs/adr/007-source-manifest.md @@ -68,3 +68,23 @@ from it every fact the code needs. coordinates that are well-formed and misplaced. - A malformed record blocks every build, not just the import that reads it. - Fields no code reads have only review keeping them current. + +## Revision + +2026-09-14: the manifest was replaced by a declaration on the class that maps +each data set, stated as an attribute whose type is the metadata type itself. + +Two factors caused the change in approach. +Each data set was named twice, once as a manifest key and +once in the class mapping it, and nothing checked that the two agreed, so a +rename on one side produced a data set that could no longer be selected. And a +record was reachable only through the configuration it belonged to, leaving the +metadata unavailable to anything that merely wanted to describe the published +data sets. + +Declaring it on the class settles both: one name, in one file, readable without +constructing anything. A malformed declaration still fails, now because the +type rejects one that omits a required fact rather than because a schema +validates it. Two trade-offs arrive with it: attribute arguments must be +constant expressions, so metadata taken from the environment needs another +mechanism, and no single file lists every published data set. diff --git a/docs/adr/README.md b/docs/adr/README.md index 0560124..160ae3d 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -18,5 +18,5 @@ Numbering follows dependency order: each ADR relies only on lower-numbered ones. Dates therefore do not run in the same order as numbers. All ADRs state general policy and name no data set. Concrete per-data-set -facts are recorded in `config/sources.yaml`, and the mappings in the source -classes themselves. +facts are declared on the source classes themselves, alongside the mappings +they describe. From dca96f5f6858f208620199a2dc274ae37aebb520 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 14 Sep 2026 12:30:32 +0200 Subject: [PATCH 080/101] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d62a5b1..cb66a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-19](https://github.com/itk-dev/enter/pull/19) + Declare source metadata in an `#[AsDataSource]` attribute on the source class * [PR-12](https://github.com/itk-dev/enter/pull/12) Add source definition as class * [PR-9](https://github.com/itk-dev/enter/pull/9) From f0e151c366fd6cee7a60b67c8b6ab32f709009d8 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 14 Sep 2026 12:36:39 +0200 Subject: [PATCH 081/101] Adjusted comment --- src/Source/AsDataSource.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Source/AsDataSource.php b/src/Source/AsDataSource.php index 96f71f2..96dfb17 100644 --- a/src/Source/AsDataSource.php +++ b/src/Source/AsDataSource.php @@ -6,10 +6,6 @@ /** * Declares the data set a source class publishes. - * - * The attribute is the definition rather than a description of one, so the - * fields a source states are the fields the application reads, with nothing - * mapping one onto the other. */ #[\Attribute(\Attribute::TARGET_CLASS)] final readonly class AsDataSource extends Definition From db8773559a197e4583f3eb2156a0881d23884917 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 14 Sep 2026 12:45:52 +0200 Subject: [PATCH 082/101] Coding standards --- docs/adr/007-source-manifest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/007-source-manifest.md b/docs/adr/007-source-manifest.md index 7bd7f8f..8dcd987 100644 --- a/docs/adr/007-source-manifest.md +++ b/docs/adr/007-source-manifest.md @@ -74,7 +74,7 @@ from it every fact the code needs. 2026-09-14: the manifest was replaced by a declaration on the class that maps each data set, stated as an attribute whose type is the metadata type itself. -Two factors caused the change in approach. +Two factors caused the change in approach. Each data set was named twice, once as a manifest key and once in the class mapping it, and nothing checked that the two agreed, so a rename on one side produced a data set that could no longer be selected. And a From 25ac8ba7c5483fe6a2ae226e6b6b5b45de8af5c7 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 14 Sep 2026 13:23:42 +0200 Subject: [PATCH 083/101] Remove extra AsDataSource layer --- src/Source/AsDataSource.php | 13 ------------- src/Source/Definition.php | 1 + src/Source/MtmSpatialMaps/HandicapParking.php | 4 ++-- src/Source/Osm/HandicapParking.php | 4 ++-- 4 files changed, 5 insertions(+), 17 deletions(-) delete mode 100644 src/Source/AsDataSource.php diff --git a/src/Source/AsDataSource.php b/src/Source/AsDataSource.php deleted file mode 100644 index 96dfb17..0000000 --- a/src/Source/AsDataSource.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Mon, 14 Sep 2026 15:58:21 +0200 Subject: [PATCH 084/101] Fixed typo --- src/Source/Definition.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Source/Definition.php b/src/Source/Definition.php index f65a5fd..a50e994 100644 --- a/src/Source/Definition.php +++ b/src/Source/Definition.php @@ -46,8 +46,8 @@ public function __construct( public static function of(string $class): self { $reflection = new \ReflectionClass($class); - $attribute = $reflection->getAttributes(AsDataSource::class)[0] - ?? throw new \LogicException(sprintf('Source %s declares no #[%s] attribute.', $class, AsDataSource::class)); + $attribute = $reflection->getAttributes(Definition::class)[0] + ?? throw new \LogicException(sprintf('Source %s declares no #[%s] attribute.', $class, Definition::class)); return $attribute->newInstance(); } From 0cc7f626635ac231c330e9c58ee409dedcba9e38 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 9 Sep 2026 16:08:53 +0200 Subject: [PATCH 085/101] Added test data setup --- CHANGELOG.md | 2 + docs/Testing.md | 12 +++ src/Controller/DataController.php | 9 -- src/Controller/DefaultController.php | 2 +- src/Source/Test/TestHandicapParking.php | 98 +++++++++++++++++++ templates/default/index.html.twig | 84 +++++----------- templates/test/index.html.twig | 12 +++ tests/resources/README.md | 9 ++ .../config/Parking/OnStreetParking.yaml | 25 +++++ tests/resources/data/fetch-resources | 17 ++++ ...page=get_geojson_opendata&datasource=invap | 1 + 11 files changed, 204 insertions(+), 67 deletions(-) create mode 100644 docs/Testing.md create mode 100644 src/Source/Test/TestHandicapParking.php create mode 100644 templates/test/index.html.twig create mode 100644 tests/resources/README.md create mode 100644 tests/resources/config/Parking/OnStreetParking.yaml create mode 100755 tests/resources/data/fetch-resources create mode 100644 tests/resources/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap diff --git a/CHANGELOG.md b/CHANGELOG.md index cb66a86..bd7d43b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-10](https://github.com/itk-dev/enter/pull/10) + Added test data setup * [PR-19](https://github.com/itk-dev/enter/pull/19) Declare source metadata in an `#[AsDataSource]` attribute on the source class * [PR-12](https://github.com/itk-dev/enter/pull/12) diff --git a/docs/Testing.md b/docs/Testing.md new file mode 100644 index 0000000..9c66a2f --- /dev/null +++ b/docs/Testing.md @@ -0,0 +1,12 @@ +# Testing + +For local testing and development we use a test controller that's only enabled in the `dev` and `test` environments. + +Load some local test data from a test data source (only available in the `dev` and `test` environments): + +```shell +docker compose exec phpfpm php bin/console app:source:import test:mtm_spatialmaps-handicap-parking +curl 'http://enter.local.itkdev.dk/data/ngsi-ld/v1/entities?type=https://smartdatamodels.org/dataModel.Parking/OnStreetParking' +``` + +See the result on . diff --git a/src/Controller/DataController.php b/src/Controller/DataController.php index 25ccb2f..fb8c43f 100644 --- a/src/Controller/DataController.php +++ b/src/Controller/DataController.php @@ -6,7 +6,6 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\HttpClient\HttpClientInterface; final class DataController extends AbstractController @@ -54,12 +53,4 @@ public function index(Request $request, string $path, string $_format, json: true, ); } - - #[Route('/test', name: 'data_test')] - public function test(): JsonResponse - { - $data = Yaml::parseFile(__DIR__.'/data.yaml'); - - return new JsonResponse($data); - } } diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 674b131..78d9f59 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -10,7 +10,7 @@ final class DefaultController extends AbstractController { - #[Route('/{path}', name: 'app_default', requirements: ['path' => Requirement::CATCH_ALL], methods: [Request::METHOD_GET])] + #[Route('/{path}', name: 'app_default', requirements: ['path' => Requirement::CATCH_ALL], methods: [Request::METHOD_GET], priority: -9999)] public function index(?string $path = null): Response { return $this->render(null === $path ? 'default/index.html.twig' : sprintf('default/%s.html.twig', $path)); diff --git a/src/Source/Test/TestHandicapParking.php b/src/Source/Test/TestHandicapParking.php new file mode 100644 index 0000000..fe886bc --- /dev/null +++ b/src/Source/Test/TestHandicapParking.php @@ -0,0 +1,98 @@ +definition = new Definition( + id: 'test:mtm_spatialmaps-handicap-parking', + title: 'Test: Handicapparkering, Aarhus Kommune', + description: 'Disabled parking bays in Aarhus Municipality, with the number of reserved bays per location.', + publisher: 'Aarhus Kommune', + contact: 'ppg@aarhus.dk', + landingPage: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune', + accessUrl: 'http://nginx:8080/test/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', + mediaType: 'application/geo+json', + crs: 'EPSG:25832', + model: 'OnStreetParking', + contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', + updateFrequency: 'continuous', + licence: null, + + omittedFields: [ + 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', + 'oprettet_af' => 'Directory username of the municipal employee who created the record.', + 'rettet_af' => 'Directory username of the municipal employee who last edited the record.', + 'oprettet_dato' => 'Describes the register record.', + 'rettet_dato' => 'Describes the register record.', + 'mi_style' => 'MapInfo rendering style, empty throughout the export.', + ], + ); + } + + /** + * Maps one feed record onto an NgsiEntity. + * + * @param array $data GeoJSON Feature + */ + public function createNgsiEntity(array $data): ?NgsiEntity + { + $row = $data['properties'] ?? null; + $geometry = $data['geometry'] ?? null; + + if (!\is_array($row) || !\is_array($geometry)) { + return null; + } + + // mi_prinx is the feed's stable primary key. Without it there is no + // way to address the same bay again on the next import, and an upsert + // would create duplicates instead of updating. + $key = $row['mi_prinx'] ?? null; + if (null === $key || '' === $key) { + return null; + } + + $entity = new NgsiEntity( + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-%s', $this->definition->model, $key), + $this->definition->model + ); + + return $entity + ->setProperty('name', $this->address($row)) + ->setProperty('description', trim((string) ($row['bemrk'] ?? ''))) + ->setProperty('category', ['forDisabled']) + ->setProperty('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) + ->setProperty('source', $this->definition->accessUrl) + ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); + } + + /** + * @param array $row + */ + private function address(array $row): string + { + return trim(\sprintf( + '%s %s', + trim((string) ($row['vejnavn'] ?? '')), + trim((string) ($row['husnnr'] ?? '')) + )); + } +} diff --git a/templates/default/index.html.twig b/templates/default/index.html.twig index 248b9b6..38b64ee 100644 --- a/templates/default/index.html.twig +++ b/templates/default/index.html.twig @@ -1,61 +1,31 @@ {% extends 'base.html.twig' %} {% block content %} - {% for path in ['hundeskov'] %} - {{ path }} - {% endfor %} - -
#map
- - - -{# -4.9 NGSI-LD Query Language: https://cim.etsi.org/NGSI-LD/official/clause-4.html#4.9 -4.10 NGSI-LD Geoquery Language: https://cim.etsi.org/NGSI-LD/official/clause-4.html#4.10 -#} -
    -{% for query in [ -{type: 'handicapparkering'}, -{type: 'hundeskov'}, -{type: 'toilet'}, -{q: 'beskrivelse==Handicaptoilet'}, - -{ -georel: 'near;maxDistance==20000', -geometry: 'Point', -coordinates: [10, 56]|json_encode, -}, - -{ -georel: 'near;maxDistance==2000', -geometry: 'Point', -coordinates: [10.236808047161853, 56.101226991176155]|json_encode, -}, - -{ -georel: 'near;maxDistance==8000', -geometry: 'Point', -coordinates: [10.236808047161853, 56.101226991176155]|json_encode, -}, - -] %} -{% set url = path('app_data', {path: 'ngsi-ld/v1/entities', _format: 'geojson'}|merge(query)) %} - -{# -Scorpio complains about "Bad Request Data." and says "Georelation is not valid" when `==` is URL encoded! -#} -{% set url = url|replace({('maxDistance=='|url_encode): 'maxDistance=='}) %} - -
  • {{ url }}
  • -{% endfor %} -
- -Reset -
-{# #} -{# https://github.com/Septima/widget-issues/issues/24#issuecomment-5478400091 #} - - -https://enter.ddev.site:33001/data/ngsi-ld/v1/entities.geojson?georel=near;maxDistance==2000&geometry=Point&coordinates=[8,40] - + + +
+
+ +
+
+ + {% if 'dev' == app.environment %} +

You're probably looking for the test controller, right?

+ {% endif %} {% endblock %} diff --git a/templates/test/index.html.twig b/templates/test/index.html.twig new file mode 100644 index 0000000..dc0fb8a --- /dev/null +++ b/templates/test/index.html.twig @@ -0,0 +1,12 @@ +{% extends 'base.html.twig' %} + +{% block content %} + {% set type = 'https://smartdatamodels.org/dataModel.Parking/OnStreetParking' %} + +

{{ type }}

+ +
+ {# #} + {# https://github.com/Septima/widget-issues/issues/24#issuecomment-5478400091 #} + +{% endblock %} diff --git a/tests/resources/README.md b/tests/resources/README.md new file mode 100644 index 0000000..63a8c9b --- /dev/null +++ b/tests/resources/README.md @@ -0,0 +1,9 @@ +# Test data resources + +Run + +```shell +./data/fetch-resources +``` + +to update the data resources. diff --git a/tests/resources/config/Parking/OnStreetParking.yaml b/tests/resources/config/Parking/OnStreetParking.yaml new file mode 100644 index 0000000..f3e6c98 --- /dev/null +++ b/tests/resources/config/Parking/OnStreetParking.yaml @@ -0,0 +1,25 @@ +debug: true +map: + # "srs": "EPSG:4326", + + view: + zoomLevel: 6 + # Dokk1 + x: 575436.27797098 + y: 6223832.5677597 + # x: 10.214391917362704, + # y: 56.153720859591054 + + layer: + - namedlayer: "#septima_standard" + + - features_host: "/data/ngsi-ld/v1/entities.geojson?type=https://smartdatamodels.org/dataModel.Parking/OnStreetParking" + + # THe undocumented property src is needed to display GeoJSON! + srs: "EPSG:4326" + + visible: true + features: true + + features_style: + - namedstyle: "#001" diff --git a/tests/resources/data/fetch-resources b/tests/resources/data/fetch-resources new file mode 100755 index 0000000..77b20c9 --- /dev/null +++ b/tests/resources/data/fetch-resources @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -o errexit -o errtrace -o noclobber -o nounset -o pipefail +IFS=$'\n\t' + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +cd "$script_dir" + +urls=( + "https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap" +) +for url in "${urls[@]}"; do + target_file_name="${url##https://}" + mkdir -p "$(dirname "$target_file_name")" + curl --silent "$url" >| "$target_file_name" + echo "$url → $target_file_name" +done diff --git a/tests/resources/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap b/tests/resources/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap new file mode 100644 index 0000000..77f0634 --- /dev/null +++ b/tests/resources/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap @@ -0,0 +1 @@ +{ "type": "FeatureCollection","crs": {"type": "name", "properties": {"name": "EPSG:25832"}}, "bbox": [570415.7999999999,6211070.354199971,577384.3500328106,6232262.860918405],"features": [{"type": "Feature","geometry": {"type": "Point","coordinates": [575153.9524951308, 6224260.609753487]},"properties": {"vejnavn": "Domkirkeplads/Bispegade", "husnnr": "1", "invalidepladser": 6, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:23.91", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:23.91", "mi_style": null, "mi_prinx": 172}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574656.6043964146, 6223886.745852402]},"properties": {"vejnavn": "Christiansgade", "husnnr": "2", "invalidepladser": 5, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:23.943", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:23.943", "mi_style": null, "mi_prinx": 173}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574519.3323188772, 6223394.810052871]},"properties": {"vejnavn": "Krigersvej", "husnnr": "5", "invalidepladser": 4, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:04:59.57", "mi_style": null, "mi_prinx": 175}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574916.9504383737, 6223961.870560985]},"properties": {"vejnavn": "Telefonsmøgen / Busgaden", "husnnr": "6", "invalidepladser": 3, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.113", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.113", "mi_style": null, "mi_prinx": 176}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574742.8394360626, 6224182.215710367]},"properties": {"vejnavn": "Grønnegade", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.15", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.15", "mi_style": null, "mi_prinx": 177}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574727.5186573606, 6224387.563912335]},"properties": {"vejnavn": "Klostergade 1", "husnnr": "74", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.193", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.193", "mi_style": null, "mi_prinx": 178}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574855.8060164271, 6224401.099589322]},"properties": {"vejnavn": "Klostergade 2", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.26", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:04.027", "mi_style": null, "mi_prinx": 179}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574504.0892603148, 6223553.9135994455]},"properties": {"vejnavn": "Skt. Nicolaus Gade", "husnnr": "1 til 3", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.363", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.363", "mi_style": null, "mi_prinx": 181}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574952.5463860369, 6223535.318275151]},"properties": {"vejnavn": "Ny Banegårdsgade", "husnnr": "49", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.403", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:16:08.26", "mi_style": null, "mi_prinx": 182}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575228.2588088225, 6224049.9438427165]},"properties": {"vejnavn": "Kannikegade", "husnnr": "16A", "invalidepladser": 2, "bemrk": "Tidsbegrænset", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.46", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:57:11.063", "mi_style": null, "mi_prinx": 183}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575319.8423906788, 6224763.017489845]},"properties": {"vejnavn": "Nørreport", "husnnr": "22", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.51", "mi_style": null, "mi_prinx": 184}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574890.2916237647, 6223681.297670785]},"properties": {"vejnavn": "Rosenkrantzgade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.557", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 14:37:23.03", "mi_style": null, "mi_prinx": 185}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574751.5470476393, 6223649.994042548]},"properties": {"vejnavn": "Park Alle 2", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.61", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.61", "mi_style": null, "mi_prinx": 186}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575067.4865718425, 6224480.395016832]},"properties": {"vejnavn": "Borggade 1", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.657", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.657", "mi_style": null, "mi_prinx": 187}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574636.8473534812, 6223433.838040478]},"properties": {"vejnavn": "Orla Lehmanns Alle", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.743", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.743", "mi_style": null, "mi_prinx": 188}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574714.8, 6223554.750000001]},"properties": {"vejnavn": "Johannes Bjergs Gade", "husnnr": "", "invalidepladser": 1, "bemrk": "På hjørner ved Banegårdspladsen 14", "ident": "1185", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.773", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:49:33.863", "mi_style": null, "mi_prinx": 189}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574779.9704729278, 6223780.417495232]},"properties": {"vejnavn": "Rådhuspladsen", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.81", "mi_style": null, "mi_prinx": 190}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575021.1449268986, 6223906.111910498]},"properties": {"vejnavn": "Ferdinand Sallings Stræde", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.847", "mi_style": null, "mi_prinx": 191}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575028.063190263, 6223924.808104941]},"properties": {"vejnavn": "Ferdinand Sallings Stræde", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.873", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.873", "mi_style": null, "mi_prinx": 192}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574539.4144572791, 6224578.814983732]},"properties": {"vejnavn": "Grønnegade", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.907", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.907", "mi_style": null, "mi_prinx": 193}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574533.8732499122, 6224584.343858345]},"properties": {"vejnavn": "Grønnegade", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.933", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.933", "mi_style": null, "mi_prinx": 194}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575460.8322248239, 6224695.803278689]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.97", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:10:54.057", "mi_style": null, "mi_prinx": 195}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575461.5348009364, 6224697.442622952]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.003", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:10:30.637", "mi_style": null, "mi_prinx": 196}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575455.3216124629, 6226039.447676172]},"properties": {"vejnavn": "Steen Billes Torv", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.037", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.037", "mi_style": null, "mi_prinx": 197}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573959.1296566332, 6223167.542244217]},"properties": {"vejnavn": "Ankersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.07", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.07", "mi_style": null, "mi_prinx": 198}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573965.1903521908, 6223169.331879942]},"properties": {"vejnavn": "Ankersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.093", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.093", "mi_style": null, "mi_prinx": 199}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573771.8681415829, 6222925.054833357]},"properties": {"vejnavn": "Læssøesgade", "husnnr": "85", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.127", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:14:58.7", "mi_style": null, "mi_prinx": 200}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573866.0093369979, 6222685.91027901]},"properties": {"vejnavn": "Harald Jenesens Plads", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.163", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.163", "mi_style": null, "mi_prinx": 201}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573940.9310782718, 6222759.405319308]},"properties": {"vejnavn": "Harald Jenesens Plads", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.19", "mi_style": null, "mi_prinx": 202}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574372.5907076919, 6222033.218436124]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.297", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:25.34", "mi_style": null, "mi_prinx": 203}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574368.4426301755, 6222031.638216118]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.323", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:16.533", "mi_style": null, "mi_prinx": 204}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574370.6154326841, 6222032.23079862]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.35", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:06.723", "mi_style": null, "mi_prinx": 205}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574136.7129774133, 6222538.070841889]},"properties": {"vejnavn": "Chr. Wærums Vej", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.387", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:47:59.483", "mi_style": null, "mi_prinx": 206}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574736.8941823252, 6222561.355631749]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.427", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.427", "mi_style": null, "mi_prinx": 207}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574086.8495389346, 6223044.81722454]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.5", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.5", "mi_style": null, "mi_prinx": 209}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575326.9832918391, 6224757.308651863]},"properties": {"vejnavn": "Nørreport", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.53", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.53", "mi_style": null, "mi_prinx": 210}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575469.2735810118, 6224466.787786544]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.567", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.567", "mi_style": null, "mi_prinx": 211}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575471.3432879301, 6224465.178114189]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.59", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.59", "mi_style": null, "mi_prinx": 212}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575473.4129948484, 6224463.568441832]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.617", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.617", "mi_style": null, "mi_prinx": 213}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575475.4744559224, 6224461.968767442]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.647", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.647", "mi_style": null, "mi_prinx": 214}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575477.5441628407, 6224460.129141892]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.673", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.673", "mi_style": null, "mi_prinx": 215}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575479.1521024784, 6224458.289516343]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.773", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.773", "mi_style": null, "mi_prinx": 216}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575481.2218093968, 6224456.219937599]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.797", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.797", "mi_style": null, "mi_prinx": 217}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575483.0523868304, 6224454.620263209]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.823", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.823", "mi_style": null, "mi_prinx": 218}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575485.1220937488, 6224452.320731272]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.85", "mi_style": null, "mi_prinx": 219}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575487.1918006671, 6224449.791246141]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.88", "mi_style": null, "mi_prinx": 220}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575515.433817381, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.907", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.907", "mi_style": null, "mi_prinx": 221}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575517.96529158, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.93", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.93", "mi_style": null, "mi_prinx": 222}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575520.9502872152, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.957", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.957", "mi_style": null, "mi_prinx": 223}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575523.9352828503, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.983", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.983", "mi_style": null, "mi_prinx": 224}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575526.9202784855, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.007", "mi_style": null, "mi_prinx": 225}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575529.6743904804, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.033", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.033", "mi_style": null, "mi_prinx": 226}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575532.6593861155, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.067", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.067", "mi_style": null, "mi_prinx": 227}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575535.4217439546, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.09", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.09", "mi_style": null, "mi_prinx": 228}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575538.4067395899, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.117", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.117", "mi_style": null, "mi_prinx": 229}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575540.9299679445, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.157", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.157", "mi_style": null, "mi_prinx": 230}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574394.576202219, 6223499.304714926]},"properties": {"vejnavn": "Valdemarsgade", "husnnr": "20", "invalidepladser": 1, "bemrk": null, "ident": "1102", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.19", "mi_style": null, "mi_prinx": 231}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575033.2828097024, 6222970.362379608]},"properties": {"vejnavn": "Heibergsgade", "husnnr": "27", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.223", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.223", "mi_style": null, "mi_prinx": 232}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573499.0, 6224197.1000000015]},"properties": {"vejnavn": "Regensburgsgade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.257", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:24:06.55", "mi_style": null, "mi_prinx": 233}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574459.3308174759, 6223990.544724442]},"properties": {"vejnavn": "Marstrandsgade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.287", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.287", "mi_style": null, "mi_prinx": 234}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574218.7335726058, 6224300.371660066]},"properties": {"vejnavn": "Mønsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.32", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.32", "mi_style": null, "mi_prinx": 235}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574841.6081590408, 6224887.112230719]},"properties": {"vejnavn": "Sjællandsgade", "husnnr": "92", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.353", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.353", "mi_style": null, "mi_prinx": 236}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224497.9]},"properties": {"vejnavn": "Tage Hansens Gade", "husnnr": "31B", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.38", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:18.623", "mi_style": null, "mi_prinx": 237}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573350.8007395364, 6225946.216653077]},"properties": {"vejnavn": "Hammershusvej", "husnnr": "41", "invalidepladser": 1, "bemrk": "Hammershusvej mellem nr. 41 og nr. 43", "ident": "1109", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.413", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.413", "mi_style": null, "mi_prinx": 238}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575124.3828974852, 6225601.696779101]},"properties": {"vejnavn": "Skt. Johannes Allé", "husnnr": "4", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": "1110", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.443", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:00:33.773", "mi_style": null, "mi_prinx": 239}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575252.1357631639, 6224570.226731846]},"properties": {"vejnavn": "Studsgade", "husnnr": "44", "invalidepladser": 2, "bemrk": null, "ident": "1111", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.473", "mi_style": null, "mi_prinx": 240}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574824.2671484861, 6226340.746347698]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "58", "invalidepladser": 1, "bemrk": null, "ident": "1111", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.507", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.507", "mi_style": null, "mi_prinx": 241}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575281.3650102671, 6223652.100041065]},"properties": {"vejnavn": "Toldbodgade", "husnnr": "6", "invalidepladser": 1, "bemrk": "Ud for svømmehallen", "ident": "1112", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.537", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:31:09.807", "mi_style": null, "mi_prinx": 242}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573871.8803781366, 6222609.255881792]},"properties": {"vejnavn": "Carl Bertelsens Gade", "husnnr": "3", "invalidepladser": 1, "bemrk": "I nærheden af Carl Bertelsens Gade nr. 3", "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.567", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.567", "mi_style": null, "mi_prinx": 243}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574261.0265080002, 6223960.050931365]},"properties": {"vejnavn": "Jerichausgade", "husnnr": "24", "invalidepladser": 1, "bemrk": "I den eksisterende skråparkering ud for nr. 24", "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.657", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.657", "mi_style": null, "mi_prinx": 245}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573354.775236487, 6224149.5423609335]},"properties": {"vejnavn": "Silkeborgvej", "husnnr": "108", "invalidepladser": 1, "bemrk": null, "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.717", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.717", "mi_style": null, "mi_prinx": 246}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574302.3464337677, 6219824.742661404]},"properties": {"vejnavn": "Kridthøjtorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.763", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.763", "mi_style": null, "mi_prinx": 247}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574881.526291278, 6222643.79885067]},"properties": {"vejnavn": "Assensgade", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1114", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.92", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.92", "mi_style": null, "mi_prinx": 248}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574120.6245171713, 6222766.663841857]},"properties": {"vejnavn": "Ingerslev Boulevard", "husnnr": "19", "invalidepladser": 1, "bemrk": "I nærheden af Ingerslev Boulevard nr. 19", "ident": "1114", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.0", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.0", "mi_style": null, "mi_prinx": 250}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574835.6134302375, 6222775.322079497]},"properties": {"vejnavn": "Hans Broges Gade", "husnnr": "17", "invalidepladser": 2, "bemrk": "Borger ønsker pladsen tidsbegrænset, da det er ved arbejdspladsen", "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.083", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.083", "mi_style": null, "mi_prinx": 252}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575090.5666900271, 6223833.916605643]},"properties": {"vejnavn": "Østergade", "husnnr": "35", "invalidepladser": 1, "bemrk": null, "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.143", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.143", "mi_style": null, "mi_prinx": 253}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575371.6015553251, 6224793.341317518]},"properties": {"vejnavn": "Helgenæsgade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.187", "mi_style": null, "mi_prinx": 254}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574443.9523178637, 6224528.715181402]},"properties": {"vejnavn": "Sejrøgade", "husnnr": "11", "invalidepladser": 1, "bemrk": "I nærheden af Sejrøgade nr.11", "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.243", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.243", "mi_style": null, "mi_prinx": 255}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575462.4460219346, 6224699.380442982]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 3, "bemrk": "Den ene af tre eksisterende HC-pladser ud for nr. 53-55 tidsbegrænset fra kl.8.00-14.00", "ident": "1118", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.29", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.29", "mi_style": null, "mi_prinx": 256}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572618.3471282512, 6224015.439657151]},"properties": {"vejnavn": "Silkeborgvej", "husnnr": "226", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.343", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.343", "mi_style": null, "mi_prinx": 257}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572934.9133366208, 6219773.593072755]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "137", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.393", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.393", "mi_style": null, "mi_prinx": 258}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572966.1156114371, 6219796.788351424]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "135", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.453", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.453", "mi_style": null, "mi_prinx": 259}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573958.7998228613, 6222500.128094437]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.513", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.513", "mi_style": null, "mi_prinx": 260}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574108.2557507273, 6222343.6199512165]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "2", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.57", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.57", "mi_style": null, "mi_prinx": 261}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574964.7928269794, 6222704.936406298]},"properties": {"vejnavn": "Strandparken", "husnnr": "36", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.613", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.613", "mi_style": null, "mi_prinx": 262}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574986.6525602082, 6222771.112936256]},"properties": {"vejnavn": "Strandparken", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.65", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.65", "mi_style": null, "mi_prinx": 263}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574174.5193554903, 6222819.103167984]},"properties": {"vejnavn": "Ingerslevs Boulevard", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.7", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.7", "mi_style": null, "mi_prinx": 264}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574259.3031265422, 6222874.021989416]},"properties": {"vejnavn": "Lundinsgade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.753", "mi_style": null, "mi_prinx": 265}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574732.0621175678, 6222787.729553992]},"properties": {"vejnavn": "Tietgens Plads", "husnnr": "11", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.81", "mi_style": null, "mi_prinx": 266}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574820.7791563489, 6223119.082108137]},"properties": {"vejnavn": "Brammersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.857", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.857", "mi_style": null, "mi_prinx": 267}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574401.956232864, 6223179.749759412]},"properties": {"vejnavn": "Godthåbsgade", "husnnr": null, "invalidepladser": 1, "bemrk": "Overfor nr. 2", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.963", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.963", "mi_style": null, "mi_prinx": 268}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574564.0530400358, 6223225.930359485]},"properties": {"vejnavn": "Dybbølsgade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.01", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.01", "mi_style": null, "mi_prinx": 269}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574140.167168153, 6223217.512073003]},"properties": {"vejnavn": "Absolonsgade", "husnnr": "28", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.043", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.043", "mi_style": null, "mi_prinx": 270}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574275.7206025358, 6223430.478724257]},"properties": {"vejnavn": "Sonnesgade", "husnnr": "7", "invalidepladser": 3, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.083", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.083", "mi_style": null, "mi_prinx": 271}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574256.1861973983, 6223461.63238302]},"properties": {"vejnavn": "Echersbergsgade", "husnnr": "43", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.15", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.15", "mi_style": null, "mi_prinx": 273}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574374.357392005, 6223467.071275949]},"properties": {"vejnavn": "Valdemarsgade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.187", "mi_style": null, "mi_prinx": 274}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574769.5764453339, 6223465.5386415245]},"properties": {"vejnavn": "Banegårdspladsen", "husnnr": "17", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.223", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:53:06.79", "mi_style": null, "mi_prinx": 275}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574924.5695985033, 6223516.741165786]},"properties": {"vejnavn": "Ny Banegårdsgade", "husnnr": "40", "invalidepladser": 2, "bemrk": "Overfor nr. 45", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.26", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.26", "mi_style": null, "mi_prinx": 276}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575241.6470492193, 6223772.769052051]},"properties": {"vejnavn": "Ridderstræde", "husnnr": null, "invalidepladser": 1, "bemrk": "Overfor nr 1", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.3", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.3", "mi_style": null, "mi_prinx": 277}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575132.5344444042, 6223962.394105587]},"properties": {"vejnavn": "Fiskergade", "husnnr": "84", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:17:48.05", "mi_style": null, "mi_prinx": 279}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574746.0553153381, 6224061.31032031]},"properties": {"vejnavn": "Christiansgade", "husnnr": "29S", "invalidepladser": 3, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.437", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.437", "mi_style": null, "mi_prinx": 281}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575181.3781732596, 6224118.438691995]},"properties": {"vejnavn": "Teatergaden", "husnnr": null, "invalidepladser": 3, "bemrk": "Sydlig endeaf vejen, på nordlig side", "ident": "E-8686", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.473", "mi_style": null, "mi_prinx": 282}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575222.3270360337, 6224141.414015435]},"properties": {"vejnavn": "Teatergaden", "husnnr": null, "invalidepladser": 4, "bemrk": "Nordlig ende af vejen , nordlig side", "ident": "E-8686", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.51", "mi_style": null, "mi_prinx": 283}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.7847335092, 6224144.713343865]},"properties": {"vejnavn": "Grønnegade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.587", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.587", "mi_style": null, "mi_prinx": 285}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574634.6704505871, 6224881.683335755]},"properties": {"vejnavn": "Falstergade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.66", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.66", "mi_style": null, "mi_prinx": 287}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573664.0273809661, 6224806.278684154]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "12", "invalidepladser": 2, "bemrk": "Overfor nr. 39", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.703", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.703", "mi_style": null, "mi_prinx": 288}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573900.8397833044, 6224443.072613699]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.74", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.74", "mi_style": null, "mi_prinx": 289}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573876.0610211948, 6224382.904860672]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.777", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.777", "mi_style": null, "mi_prinx": 290}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573858.2335058267, 6224344.252728201]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "27", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.81", "mi_style": null, "mi_prinx": 291}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574637.7049212881, 6224717.846684231]},"properties": {"vejnavn": "Hjarnøgade", "husnnr": "5", "invalidepladser": 2, "bemrk": "Overfor nr 5", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.843", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.843", "mi_style": null, "mi_prinx": 292}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574863.3524504495, 6224285.3847106155]},"properties": {"vejnavn": "Vestergade", "husnnr": "22", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.88", "mi_style": null, "mi_prinx": 293}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574537.1138667205, 6224140.554190449]},"properties": {"vejnavn": "Møllegade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.917", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.917", "mi_style": null, "mi_prinx": 294}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575074.9490609304, 6224661.438166022]},"properties": {"vejnavn": "Anholtsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.963", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.963", "mi_style": null, "mi_prinx": 295}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572960.45896225, 6225756.165337472]},"properties": {"vejnavn": "Musvågevej", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.073", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:29.073", "mi_style": null, "mi_prinx": 296}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575340.1766427128, 6224351.371279241]},"properties": {"vejnavn": "Olufgade", "husnnr": "3", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.113", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:00:13.91", "mi_style": null, "mi_prinx": 297}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573871.0393020185, 6222888.768987707]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:09:34.883", "mi_style": null, "mi_prinx": 298}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574914.8559939226, 6222594.038979148]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "34", "invalidepladser": 1, "bemrk": "5x3,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:19.733", "mi_style": null, "mi_prinx": 299}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575638.25, 6226153.625]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "222", "invalidepladser": 1, "bemrk": "5x3,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.26", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:56:12.1", "mi_style": null, "mi_prinx": 301}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574462.8600388346, 6226572.059264624]},"properties": {"vejnavn": "Stockholmsgade", "husnnr": "1", "invalidepladser": 1, "bemrk": "5x2,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.293", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:19.97", "mi_style": null, "mi_prinx": 302}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574497.212226172, 6224409.829380264]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "3", "invalidepladser": 1, "bemrk": "Til højre for porten", "ident": "1124", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.38", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.12", "mi_style": null, "mi_prinx": 304}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575140.6024732155, 6224850.239736008]},"properties": {"vejnavn": "Høegh Guldbergs Gade", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": "1130", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.423", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.197", "mi_style": null, "mi_prinx": 305}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574098.9791758942, 6222367.355119992]},"properties": {"vejnavn": "P. P. Ørumsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1127", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.467", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.267", "mi_style": null, "mi_prinx": 306}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573984.3949235562, 6222175.364199189]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "1129", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.507", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.337", "mi_style": null, "mi_prinx": 307}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574502.2421911926, 6222857.645322839]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.543", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:03:19.153", "mi_style": null, "mi_prinx": 308}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574505.5487747552, 6222858.265196665]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.57", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:03:07.203", "mi_style": null, "mi_prinx": 309}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574509.0697502696, 6222858.8850704925]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.6", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:02:53.353", "mi_style": null, "mi_prinx": 310}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.586833246, 6222612.475226503]},"properties": {"vejnavn": "Assensgade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": "1121", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.633", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.67", "mi_style": null, "mi_prinx": 311}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574719.5036967049, 6222608.076121928]},"properties": {"vejnavn": "Assensgade", "husnnr": "21", "invalidepladser": 2, "bemrk": null, "ident": "1128", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.677", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.737", "mi_style": null, "mi_prinx": 312}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576416.2510775031, 6232254.552609537]},"properties": {"vejnavn": "Lystruplund", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for børnehavens indgang", "ident": "1123", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.717", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.81", "mi_style": null, "mi_prinx": 313}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573121.6899557706, 6220138.458805391]},"properties": {"vejnavn": "Rundhøj Alle", "husnnr": "61", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.883", "mi_style": null, "mi_prinx": 314}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576412.5899226356, 6232262.860918405]},"properties": {"vejnavn": "Lystruplund", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for børnehavens indgang", "ident": "1123", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.783", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.953", "mi_style": null, "mi_prinx": 315}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575079.1956707429, 6224575.955565759]},"properties": {"vejnavn": "Borggade", "husnnr": "16", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.86", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.13", "mi_style": null, "mi_prinx": 317}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574136.9018138117, 6222534.351128438]},"properties": {"vejnavn": "Chr. Wærumsgade", "husnnr": "29", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.943", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.277", "mi_style": null, "mi_prinx": 319}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573894.8368086569, 6224475.935924468]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "6", "invalidepladser": 2, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.347", "mi_style": null, "mi_prinx": 320}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576295.1526081712, 6228305.306467168]},"properties": {"vejnavn": "Fortebakken", "husnnr": "34A", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.047", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.417", "mi_style": null, "mi_prinx": 321}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575393.7251555714, 6223907.781570643]},"properties": {"vejnavn": "Hack Kampmanns plads", "husnnr": "2", "invalidepladser": 3, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.09", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.487", "mi_style": null, "mi_prinx": 322}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574911.6071312699, 6222880.060760242]},"properties": {"vejnavn": "Hendrik Pontopidans gade", "husnnr": "2", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.123", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.56", "mi_style": null, "mi_prinx": 323}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575107.1408370622, 6226017.862069859]},"properties": {"vejnavn": "Herambsgade", "husnnr": "5", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.16", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.6", "mi_style": null, "mi_prinx": 324}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575153.6061696704, 6211381.751207897]},"properties": {"vejnavn": "Mallingsgårdsvej", "husnnr": "137", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.65", "mi_style": null, "mi_prinx": 325}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573888.9575216738, 6222869.90282786]},"properties": {"vejnavn": "Max Mullers Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.227", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.71", "mi_style": null, "mi_prinx": 326}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574546.3132659666, 6223169.7029371355]},"properties": {"vejnavn": "Montaangade", "husnnr": "8", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.263", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:58:27.443", "mi_style": null, "mi_prinx": 327}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573574.4692660668, 6222862.3343683975]},"properties": {"vejnavn": "Nordborg Gade", "husnnr": "41", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.3", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.947", "mi_style": null, "mi_prinx": 328}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574323.8351040032, 6223091.617698438]},"properties": {"vejnavn": "Ole Rømers Gade", "husnnr": "38", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.333", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.02", "mi_style": null, "mi_prinx": 329}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574564.1684818559, 6223099.706052078]},"properties": {"vejnavn": "Ole Rømers Gade", "husnnr": "63", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.373", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.09", "mi_style": null, "mi_prinx": 330}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573623.1032557249, 6222316.575456046]},"properties": {"vejnavn": "Rudolp Wulffs Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.17", "mi_style": null, "mi_prinx": 331}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572704.5079553006, 6226851.832317628]},"properties": {"vejnavn": "Sandkåsvej", "husnnr": "27", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.22", "mi_style": null, "mi_prinx": 332}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574883.422835466, 6227413.9079087665]},"properties": {"vejnavn": "Solhvervsvej", "husnnr": "22", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.547", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.29", "mi_style": null, "mi_prinx": 333}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574839.5219604338, 6224676.74505035]},"properties": {"vejnavn": "Thunøgade", "husnnr": "21", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.59", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:26:13.553", "mi_style": null, "mi_prinx": 334}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574010.542495819, 6222959.064679222]},"properties": {"vejnavn": "Trepkasgade", "husnnr": "6", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.64", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.437", "mi_style": null, "mi_prinx": 335}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573839.0454261499, 6225405.03680867]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "78", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.68", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.533", "mi_style": null, "mi_prinx": 336}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574665.6000000001, 6227253.1000000015]},"properties": {"vejnavn": "Vorregårds Allé", "husnnr": "95", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.717", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:50:45.603", "mi_style": null, "mi_prinx": 337}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574079.8900463488, 6222655.326504249]},"properties": {"vejnavn": "Ålborgade", "husnnr": "30", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.66", "mi_style": null, "mi_prinx": 338}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575257.8748707939, 6225740.82845925]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "86", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.79", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.73", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 339}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575579.6936819801, 6225552.636765125]},"properties": {"vejnavn": "Trøjborgvej", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for Kochs Skole", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.86", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.893", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 341}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575354.5079200993, 6225296.238954158]},"properties": {"vejnavn": "Kirkegårdsvej", "husnnr": "53", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.9", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.95", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 342}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575673.3747190277, 6225441.769331871]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44B", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.953", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.047", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 343}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575589.1104361663, 6225071.994598448]},"properties": {"vejnavn": "Skovvejen", "husnnr": "19B", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.12", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 344}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575246.1905094264, 6225228.322778299]},"properties": {"vejnavn": "Peter Sabroes Gade", "husnnr": "2-4", "invalidepladser": 10, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.04", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.21", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 345}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575188.106782205, 6225234.641492143]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 3, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.08", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.3", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 346}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575106.3244984768, 6225400.537724445]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 4, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.117", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.37", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 347}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575064.3119217885, 6225293.259560605]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.16", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.437", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 348}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575109.6475737281, 6225313.485443685]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.193", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.503", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 349}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575068.0720267874, 6225440.379614743]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.223", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.567", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 350}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.441055486, 6225637.589473247]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 5, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.273", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.637", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 351}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574422.6368103585, 6225613.944286156]},"properties": {"vejnavn": "Victor Albecks Vej", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.303", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.71", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 352}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574900.5989291347, 6225286.400956654]},"properties": {"vejnavn": "Vennelyst Boulevard", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.34", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.78", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 353}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574878.0960200508, 6225366.764598867]},"properties": {"vejnavn": "Bartholins Allé", "husnnr": null, "invalidepladser": 1, "bemrk": "V/Bartholinbygningen", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.373", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.847", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 354}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574170.8004797128, 6225423.143123182]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.41", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.913", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 355}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575117.8851721799, 6224865.706587776]},"properties": {"vejnavn": "Høegh-Guldbergs Gade", "husnnr": "49", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.033", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 357}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574990.5281070274, 6224945.140419249]},"properties": {"vejnavn": "Høegh-Guldbergs Gade", "husnnr": "6A", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.103", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 358}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574350.6093604325, 6224544.851896821]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "41", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.597", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.153", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 359}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574519.1874012209, 6224508.419312567]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.63", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.28", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 360}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574525.4130136644, 6224532.194473202]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.667", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.357", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 361}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573850.8204918046, 6224325.876468635]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.757", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.57", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 364}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573842.8467603703, 6224172.757635531]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.787", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.62", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 365}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573685.0130546996, 6224142.763740702]},"properties": {"vejnavn": "Regenburgs Plads", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.817", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.683", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 366}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573708.2663356145, 6223856.911925012]},"properties": {"vejnavn": "Åparken", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.737", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 367}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573789.7847523252, 6223920.838912859]},"properties": {"vejnavn": "Åparken", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.803", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 368}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574850.3899832161, 6222925.211569925]},"properties": {"vejnavn": "Odensegade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.923", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.9", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 369}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574954.056737706, 6223018.042674422]},"properties": {"vejnavn": "Marselisborg Allé", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.953", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.97", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 370}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574731.5426293771, 6223104.335109847]},"properties": {"vejnavn": "Sankt Pauls Gade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.987", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.04", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 371}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574744.3401797245, 6223104.785018269]},"properties": {"vejnavn": "Sankt Pauls Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.027", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.113", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 372}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574725.6138673283, 6222733.070679648]},"properties": {"vejnavn": "J. Michaelsens Gade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.123", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.31", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 375}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574543.1745622781, 6223191.127443518]},"properties": {"vejnavn": "Montanagade", "husnnr": "4A", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.153", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.35", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 376}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574802.1600399285, 6223111.60363036]},"properties": {"vejnavn": "Brammersgade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.393", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 377}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574870.3036971911, 6223125.000903384]},"properties": {"vejnavn": "Brammersgade", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.217", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.43", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 378}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.7603455347, 6223146.6087693]},"properties": {"vejnavn": "St. St. Blichers Gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:03:17.84", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 379}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574131.5007857978, 6222588.660074009]},"properties": {"vejnavn": "Chr. Wærums Gade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.293", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.507", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 380}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574200.5290566034, 6222565.141509435]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.327", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-24 13:56:15.127", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 381}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574191.027535771, 6222648.197955245]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.36", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.58", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 382}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574193.9630563403, 6222718.033740373]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.39", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.617", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 383}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575232.5353912722, 6224544.841898856]},"properties": {"vejnavn": "Studsgade", "husnnr": "38", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.423", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.673", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 384}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574716.4527343153, 6224380.4153674]},"properties": {"vejnavn": "Klostergade", "husnnr": "78", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.457", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.71", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 385}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574748.306430831, 6224145.5731688505]},"properties": {"vejnavn": "Grønnegade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.497", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.747", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 386}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574797.3197293266, 6224474.496217515]},"properties": {"vejnavn": "Gammel Munkegade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.863", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.827", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 388}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574841.4597338435, 6224276.456527921]},"properties": {"vejnavn": "Vestergade", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.897", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.863", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 389}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.8203643236, 6224302.431240844]},"properties": {"vejnavn": "Badstuegade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.927", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.9", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 390}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574934.1924987969, 6223330.958981213]},"properties": {"vejnavn": "M.P. Bruuns Gade", "husnnr": "25", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.957", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.933", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 391}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574915.3095153589, 6223254.954451715]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "124", "invalidepladser": 1, "bemrk": "8-16", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.997", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.97", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 392}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574915.6393491307, 6223263.812648655]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "97", "invalidepladser": 1, "bemrk": "8-16", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.043", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.007", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 393}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574488.7107607027, 6223237.937915382]},"properties": {"vejnavn": "Istedgade", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.077", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.043", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 394}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574767.2962480201, 6223464.91676953]},"properties": {"vejnavn": "Banegårdspladsen", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.19", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:52:54.773", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 396}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574530.2173944163, 6223395.66940094]},"properties": {"vejnavn": "Kriegersvej", "husnnr": "11", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.22", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:04:44.057", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 397}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574906.5854120937, 6223675.0989325205]},"properties": {"vejnavn": "Rosenkrantzgade", "husnnr": "24", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 14:37:07.49", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 398}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575020.6317082599, 6224083.022794313]},"properties": {"vejnavn": "Fiskergade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.287", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:55:01.553", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 399}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574872.5218293066, 6223967.619390828]},"properties": {"vejnavn": "Telefonsmøgen", "husnnr": null, "invalidepladser": 3, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.327", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.6", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 400}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574997.9, 6222462.25]},"properties": {"vejnavn": "Adolph Meyers Vej", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud mod Strandvejen", "ident": "1196", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-16 11:55:35.953", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-16 12:00:59.113", "mi_style": null, "mi_prinx": 404}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573880.2000000001, 6222878.7]},"properties": {"vejnavn": "Max Müllers Gade 11", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": "1169", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:08:44.613", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:08:44.613", "mi_style": null, "mi_prinx": 405}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573875.6000000001, 6222883.899999999]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "1169", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:11:20.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:11:20.153", "mi_style": null, "mi_prinx": 406}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574233.0, 6226255.500000001]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "51", "invalidepladser": 1, "bemrk": null, "ident": "1171", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:14:12.3", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:14:12.3", "mi_style": null, "mi_prinx": 407}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573849.4, 6222996.65]},"properties": {"vejnavn": "Læssøegade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": "1173", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:18:31.19", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:18:31.19", "mi_style": null, "mi_prinx": 408}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574396.7, 6224597.35]},"properties": {"vejnavn": "Skt. Marcus Kirkeplads", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1173", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:21:54.81", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:21:54.81", "mi_style": null, "mi_prinx": 409}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574845.2999999999, 6224678.449999999]},"properties": {"vejnavn": "Thunøgade 26B", "husnnr": "26B", "invalidepladser": 1, "bemrk": null, "ident": "1176", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:25:55.553", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:25:55.553", "mi_style": null, "mi_prinx": 410}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573925.4, 6222166.899999999]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": "1181", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:31:40.443", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:31:40.443", "mi_style": null, "mi_prinx": 412}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571908.4, 6223314.500000001]},"properties": {"vejnavn": "Henrik Hertz Vej", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": "1182", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:34:51.437", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:34:51.437", "mi_style": null, "mi_prinx": 413}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574719.1, 6223555.250000001]},"properties": {"vejnavn": "Johannes Bjergs Gade", "husnnr": "", "invalidepladser": 1, "bemrk": "På hjørnet ved Banegårdspladsen 14", "ident": "1185", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:48:20.697", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:48:20.697", "mi_style": null, "mi_prinx": 416}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575397.9500000001, 6225939.025]},"properties": {"vejnavn": "Ivar Huitfeldts Gade", "husnnr": "64A", "invalidepladser": 1, "bemrk": null, "ident": "1185", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:51:46.78", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:51:46.78", "mi_style": null, "mi_prinx": 417}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574223.2, 6222506.7]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:54:59.317", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:54:59.317", "mi_style": null, "mi_prinx": 418}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576141.0, 6225269.5]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:59:53.823", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:59:53.823", "mi_style": null, "mi_prinx": 419}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576136.6, 6225275.9]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:00:00.397", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:00:00.397", "mi_style": null, "mi_prinx": 420}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576132.525, 6225282.1125]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:00:19.683", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:01:17.037", "mi_style": null, "mi_prinx": 421}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574134.4, 6222663.3500000015]},"properties": {"vejnavn": "Ålborggade", "husnnr": "16", "invalidepladser": 1, "bemrk": "på NV hjørnet ved Chr. Wærums Gade", "ident": "1187", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:08:44.067", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:08:44.067", "mi_style": null, "mi_prinx": 422}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573822.0, 6222563.450000001]},"properties": {"vejnavn": "P. P. Ørums Gade", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": "1187", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:12:21.367", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:12:21.367", "mi_style": null, "mi_prinx": 423}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570801.2, 6216735.900000001]},"properties": {"vejnavn": "Kirketorvet", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": "1190", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:14:51.303", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:14:51.303", "mi_style": null, "mi_prinx": 424}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570806.0, 6216734.500000001]},"properties": {"vejnavn": "Kirketorvet", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": "1190", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:15:00.997", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:15:00.997", "mi_style": null, "mi_prinx": 425}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574101.2, 6223033.899999999]},"properties": {"vejnavn": "Schleppegrellsgade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:19:45.767", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:19:45.767", "mi_style": null, "mi_prinx": 426}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574902.6, 6222755.500000001]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:22:33.383", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:22:33.383", "mi_style": null, "mi_prinx": 427}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573502.0, 6224196.900000001]},"properties": {"vejnavn": "Regensburgsgade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:24:22.523", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:25:10.057", "mi_style": null, "mi_prinx": 428}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574308.35, 6223515.425000001]},"properties": {"vejnavn": "Christen Købkes Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:27:59.887", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:27:59.887", "mi_style": null, "mi_prinx": 430}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575200.2, 6224522.150000001]},"properties": {"vejnavn": "Studsgade", "husnnr": "22", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:29:06.1", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:29:06.1", "mi_style": null, "mi_prinx": 431}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574790.4, 6222623.850000001]},"properties": {"vejnavn": "Assensgade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:31:34.91", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:31:34.91", "mi_style": null, "mi_prinx": 432}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575263.2000000001, 6228819.3]},"properties": {"vejnavn": "Tværmarksvej", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "1200", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:36:06.61", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:36:06.61", "mi_style": null, "mi_prinx": 433}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574008.1, 6225215.550000001]},"properties": {"vejnavn": "Chr. Molbechs Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1202", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:42:59.763", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:42:59.763", "mi_style": null, "mi_prinx": 434}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575616.4500000001, 6226158.575000001]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "218", "invalidepladser": 1, "bemrk": "", "ident": "1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:22:30.467", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:22:30.467", "mi_style": null, "mi_prinx": 435}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574331.2000000001, 6227559.55]},"properties": {"vejnavn": "Vorregårds Allé", "husnnr": "125", "invalidepladser": 1, "bemrk": null, "ident": "1205", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:27:08.61", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:27:08.61", "mi_style": null, "mi_prinx": 436}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574076.6, 6222877.450000001]},"properties": {"vejnavn": "Vejlegade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1207", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:35:54.443", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:35:54.443", "mi_style": null, "mi_prinx": 437}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574813.4, 6227367.300000002]},"properties": {"vejnavn": "Solhvervsvej", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:36:25.903", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:58:57.687", "mi_style": null, "mi_prinx": 438}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573701.05, 6225537.125000001]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "37", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:39:11.127", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:39:11.127", "mi_style": null, "mi_prinx": 439}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570415.7999999999, 6224631.05]},"properties": {"vejnavn": "Karen Blixens Boulevard", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:42:06.563", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:42:06.563", "mi_style": null, "mi_prinx": 440}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573264.6000000001, 6225891.1000000015]},"properties": {"vejnavn": "Majdalen", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1209", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:46:08.143", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 07:00:59.987", "mi_style": null, "mi_prinx": 441}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575311.4, 6225676.650000001]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "1213", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:52:54.42", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:52:54.42", "mi_style": null, "mi_prinx": 442}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575013.2, 6224297.75]},"properties": {"vejnavn": "Badstuegade", "husnnr": "1B", "invalidepladser": 1, "bemrk": null, "ident": "1213", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:06:29.84", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:06:29.84", "mi_style": null, "mi_prinx": 443}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574438.2, 6224426.775000001]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": "1216", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:12:55.713", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:12:55.713", "mi_style": null, "mi_prinx": 444}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574116.1, 6222681.774999999]},"properties": {"vejnavn": "Chr. Wærums Gade", "husnnr": "11A", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:15:24.13", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:15:24.13", "mi_style": null, "mi_prinx": 445}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575674.7999999999, 6225450.7]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44B", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:25:59.593", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:26:38.423", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 446}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575677.0, 6225459.9]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44A", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:27:24.777", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:27:24.777", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 447}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575345.5377970586, 6224349.986968176]},"properties": {"vejnavn": "Skt. Olufs Gade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:32:05.217", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:59:53.663", "mi_style": null, "mi_prinx": 448}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575681.3, 6226046.85]},"properties": {"vejnavn": "Peder Skrams Gade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "1195+1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:44:10.597", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:44:10.597", "mi_style": null, "mi_prinx": 449}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575668.2000000001, 6226049.55]},"properties": {"vejnavn": "Peder Skrams Gade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": "1195+1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:44:22.327", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:44:22.327", "mi_style": null, "mi_prinx": 450}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224469.1]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "29", "invalidepladser": null, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:47:52.29", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:58.087", "mi_style": null, "mi_prinx": 451}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.6000000001, 6224461.9]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:48:07.09", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:44.383", "mi_style": null, "mi_prinx": 452}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224492.5]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "31B", "invalidepladser": 1, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:48:39.0", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:48:39.0", "mi_style": null, "mi_prinx": 453}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574462.6000000001, 6222419.500000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:06.373", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:06.373", "mi_style": null, "mi_prinx": 454}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574468.6000000001, 6222420.700000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:12.95", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:12.95", "mi_style": null, "mi_prinx": 455}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574475.0000000001, 6222421.900000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:20.613", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:20.613", "mi_style": null, "mi_prinx": 456}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574481.8, 6222423.1000000015]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:32.217", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:32.217", "mi_style": null, "mi_prinx": 457}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576152.8, 6225252.3]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:51:19.7", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:51:19.7", "mi_style": null, "mi_prinx": 458}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576138.6000000001, 6225293.1]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:51:52.03", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:51:52.03", "mi_style": null, "mi_prinx": 459}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576141.8, 6225288.5]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "10", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:52:02.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:52:02.153", "mi_style": null, "mi_prinx": 460}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576156.4, 6225267.7]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:52:20.473", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:52:20.473", "mi_style": null, "mi_prinx": 461}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574173.0585387659, 6222819.027722998]},"properties": {"vejnavn": "Ingerslev Boulevard", "husnnr": null, "invalidepladser": 1, "bemrk": "1200", "ident": "1", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 13:11:36.553", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 13:11:36.553", "mi_style": null, "mi_prinx": 464}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574717.2467898262, 6222608.072481987]},"properties": {"vejnavn": "Assensgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:16:10.33", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:16:10.33", "mi_style": null, "mi_prinx": 466}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574756.4414590374, 6222616.351223999]},"properties": {"vejnavn": "Assensgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:17:39.227", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:17:39.227", "mi_style": null, "mi_prinx": 467}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575071.0398722045, 6223736.640195375]},"properties": {"vejnavn": "Fredensgade", "husnnr": "36", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:23:36.633", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:23:36.633", "mi_style": null, "mi_prinx": 468}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575071.6634185021, 6223738.822607417]},"properties": {"vejnavn": "Fredensgade ", "husnnr": "36", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:24:07.44", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:24:07.44", "mi_style": null, "mi_prinx": 469}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574458.5545235131, 6224405.154470327]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:01:44.29", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:01:44.29", "mi_style": null, "mi_prinx": 470}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574456.8303596153, 6224406.480750249]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:02:10.15", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:02:10.15", "mi_style": null, "mi_prinx": 471}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574439.588720638, 6224425.8444371]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:02:36.877", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:02:36.877", "mi_style": null, "mi_prinx": 472}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574213.7462111231, 6224317.430271916]},"properties": {"vejnavn": "Mønsgade", "husnnr": "8a", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:06:38.743", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:06:38.743", "mi_style": null, "mi_prinx": 473}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574364.2445136809, 6225576.13351622]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:14:03.827", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:14:03.827", "mi_style": null, "mi_prinx": 474}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574271.226033188, 6225520.486698972]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:14:25.237", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:14:54.583", "mi_style": null, "mi_prinx": 475}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573648.4455852153, 6222703.284394252]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "9", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:18:50.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:10.813", "mi_style": null, "mi_prinx": 476}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573645.981519507, 6222704.927104725]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "9", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:19:02.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:24.89", "mi_style": null, "mi_prinx": 477}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574615.0200821363, 6226143.664271046]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:24:58.847", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:25:25.463", "mi_style": null, "mi_prinx": 478}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573440.6894959002, 6225916.164055831]},"properties": {"vejnavn": "Fuglesangs Allé ", "husnnr": "115", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:30:49.483", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:31:19.2", "mi_style": null, "mi_prinx": 479}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573441.7161899453, 6225914.829353572]},"properties": {"vejnavn": "Fuglesangs Allé", "husnnr": "115", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:31:13.193", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:31:13.193", "mi_style": null, "mi_prinx": 480}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572528.9192842386, 6223635.9144780645]},"properties": {"vejnavn": "Åbyvej ", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:33:23.21", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:33:23.21", "mi_style": null, "mi_prinx": 481}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [577384.3500328106, 6228392.545887083]},"properties": {"vejnavn": "Vestre Strandallé", "husnnr": "170", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:35:52.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:35:52.45", "mi_style": null, "mi_prinx": 482}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [577384.3500328106, 6228395.831308028]},"properties": {"vejnavn": "Vestre Strandallé", "husnnr": "170", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:36:07.74", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:36:07.74", "mi_style": null, "mi_prinx": 483}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574290.6883509178, 6223440.857930892]},"properties": {"vejnavn": "Christen Købkes Gade", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:04:38.56", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:04:38.56", "mi_style": null, "mi_prinx": 485}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574292.8885474477, 6223450.555093376]},"properties": {"vejnavn": "Christen Købkes gade", "husnnr": "25", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": "", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:05:17.92", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:05:17.92", "mi_style": null, "mi_prinx": 486}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574188.2994045179, 6222220.596509239]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:09:47.597", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:09:47.597", "mi_style": null, "mi_prinx": 487}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574594.8774293908, 6224862.872887781]},"properties": {"vejnavn": "Falstergade ", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:11:45.25", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:11:45.25", "mi_style": null, "mi_prinx": 488}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575645.3796509246, 6225033.394188903]},"properties": {"vejnavn": "Fiskerivej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:13:23.887", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:13:23.887", "mi_style": null, "mi_prinx": 489}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575687.3336841018, 6225135.283751449]},"properties": {"vejnavn": "Fiskerivej", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:21:28.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:21:28.367", "mi_style": null, "mi_prinx": 490}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573765.5234907598, 6222669.554414783]},"properties": {"vejnavn": "Gråstengade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:23:07.33", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:23:07.33", "mi_style": null, "mi_prinx": 491}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574263.9406273357, 6223167.701885861]},"properties": {"vejnavn": "Holbergsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:37:05.283", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:37:05.283", "mi_style": null, "mi_prinx": 492}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573818.8194244037, 6222564.275600996]},"properties": {"vejnavn": "P.P. Ørums Gade ", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:54:37.353", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:54:37.353", "mi_style": null, "mi_prinx": 494}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575020.143846024, 6224751.283552042]},"properties": {"vejnavn": "Sjællandsgade ", "husnnr": "44", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:55:58.737", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:55:58.737", "mi_style": null, "mi_prinx": 495}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575126.0758521565, 6225602.137577002]},"properties": {"vejnavn": "Skt. Johannes Allé", "husnnr": "4", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": "", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:58:18.097", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:58:18.097", "mi_style": null, "mi_prinx": 497}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575127.7185626287, 6225602.958932239]},"properties": {"vejnavn": "Skt. Johannes Allé ", "husnnr": "4", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:58:54.79", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:58:54.79", "mi_style": null, "mi_prinx": 498}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574192.3717864477, 6222728.518480494]},"properties": {"vejnavn": "Stadion Allé ", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:01:35.56", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:01:35.56", "mi_style": null, "mi_prinx": 499}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574516.2188090354, 6224495.0]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:04:13.31", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:04:13.31", "mi_style": null, "mi_prinx": 500}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573382.1503490765, 6222436.561601641]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:05:22.27", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:05:22.27", "mi_style": null, "mi_prinx": 501}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574586.7241332495, 6222319.2006106265]},"properties": {"vejnavn": "Marselis tværvej", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:10:21.543", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:10:21.543", "mi_style": null, "mi_prinx": 502}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575512.1720674813, 6225984.295569786]},"properties": {"vejnavn": "Herluf Trolles Gade", "husnnr": "43", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:11:37.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:11:37.07", "mi_style": null, "mi_prinx": 503}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570549.4435318275, 6223329.651950718]},"properties": {"vejnavn": "Louisevej", "husnnr": "60", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:12:39.197", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:12:39.197", "mi_style": null, "mi_prinx": 504}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575432.3019691993, 6213534.279897331]},"properties": {"vejnavn": "Skoleparken", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:15:20.913", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:15:20.913", "mi_style": null, "mi_prinx": 505}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575435.1767125258, 6213534.895913758]},"properties": {"vejnavn": "Skoleparken ", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:15:35.137", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:15:35.137", "mi_style": null, "mi_prinx": 506}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574686.8167566396, 6224112.425570103]},"properties": {"vejnavn": "Blegdammen", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:46:05.277", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:46:05.277", "mi_style": null, "mi_prinx": 507}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.8197535932, 6222559.590349074]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:48:59.607", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:48:59.607", "mi_style": null, "mi_prinx": 508}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574724.2673921969, 6222578.276180697]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:49:32.92", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:49:32.92", "mi_style": null, "mi_prinx": 509}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574662.6690560804, 6222698.830589775]},"properties": {"vejnavn": "Fåborggade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:52:16.67", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:52:16.67", "mi_style": null, "mi_prinx": 510}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573723.7641226394, 6222883.87392884]},"properties": {"vejnavn": "Haderslevgade", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:56:02.037", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:56:02.037", "mi_style": null, "mi_prinx": 511}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574950.1948158465, 6222518.823101973]},"properties": {"vejnavn": "Kildegården ", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:02:38.327", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:02:38.327", "mi_style": null, "mi_prinx": 512}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573174.3326094088, 6222324.340775997]},"properties": {"vejnavn": "Kirkedammen", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:03:44.91", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:03:44.91", "mi_style": null, "mi_prinx": 513}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574764.3736858368, 6224401.6597698275]},"properties": {"vejnavn": "Klostergade", "husnnr": "68", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:04:40.63", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:04:40.63", "mi_style": null, "mi_prinx": 514}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575484.2643121149, 6224755.312114988]},"properties": {"vejnavn": "Kystvejen", "husnnr": "63", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:05:45.95", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:05:45.95", "mi_style": null, "mi_prinx": 516}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575260.0545790552, 6225740.691868583]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "64", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:06:42.113", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:06:42.113", "mi_style": null, "mi_prinx": 517}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574122.1695396319, 6222253.575999677]},"properties": {"vejnavn": "Ordrupvej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:07:29.987", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:07:29.987", "mi_style": null, "mi_prinx": 518}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573820.2902647848, 6222564.066232556]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:08:36.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:08:36.257", "mi_style": null, "mi_prinx": 519}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573513.2284804934, 6224421.503059546]},"properties": {"vejnavn": "Thomas Nielsens Gade ", "husnnr": "4b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:35:15.567", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:35:15.567", "mi_style": null, "mi_prinx": 521}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574869.9698357292, 6224686.401437372]},"properties": {"vejnavn": "Thunøgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:36:20.32", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:36:20.32", "mi_style": null, "mi_prinx": 522}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574842.0437577005, 6224677.982546201]},"properties": {"vejnavn": "Thunøgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:36:47.17", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:36:47.17", "mi_style": null, "mi_prinx": 523}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573819.4550102665, 6222787.195071867]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:38:32.23", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:38:32.23", "mi_style": null, "mi_prinx": 524}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573819.8656878845, 6222774.258726898]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "3", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:38:44.137", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:38:44.137", "mi_style": null, "mi_prinx": 525}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573809.5987474328, 6222763.17043121]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "5", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:39:04.733", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:39:04.733", "mi_style": null, "mi_prinx": 526}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574862.9051540041, 6226224.204312115]},"properties": {"vejnavn": "Finsensgade", "husnnr": "61", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:40:40.31", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:40:40.31", "mi_style": null, "mi_prinx": 527}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575609.3253388093, 6225642.107802872]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "14", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:41:40.133", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:41:40.133", "mi_style": null, "mi_prinx": 528}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575621.7098357282, 6226157.82032854]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "220", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:42:29.26", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:42:29.26", "mi_style": null, "mi_prinx": 529}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574273.5949691997, 6226579.6673511285]},"properties": {"vejnavn": "Frederikshaldsgade ", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:46:09.1", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:46:09.1", "mi_style": null, "mi_prinx": 530}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574272.9789527727, 6226577.408624229]},"properties": {"vejnavn": "Frederikshaldsgade", "husnnr": "15", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:46:27.437", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:46:27.437", "mi_style": null, "mi_prinx": 531}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574550.2745468095, 6226415.597575942]},"properties": {"vejnavn": "Norges Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:48:32.27", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:48:32.27", "mi_style": null, "mi_prinx": 532}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573464.9690871622, 6225551.886526177]},"properties": {"vejnavn": "Fuglesangs Allé", "husnnr": "114", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:49:51.593", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:49:51.593", "mi_style": null, "mi_prinx": 533}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573284.2962628339, 6226064.2587268995]},"properties": {"vejnavn": "Hammershusvej ", "husnnr": "51", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:50:32.377", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:50:32.377", "mi_style": null, "mi_prinx": 534}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574168.5019342923, 6225689.519246406]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "92d", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:52:34.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:52:34.45", "mi_style": null, "mi_prinx": 536}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573183.6454293831, 6219583.845422071]},"properties": {"vejnavn": "M.P. Hansens Vej", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:54:27.033", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:54:27.033", "mi_style": null, "mi_prinx": 537}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573194.0588431241, 6219583.965116482]},"properties": {"vejnavn": "M.P. Hansens Gade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:54:45.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:54:45.367", "mi_style": null, "mi_prinx": 538}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574293.6617659132, 6222509.108829569]},"properties": {"vejnavn": "F. Vestergaards Gade", "husnnr": "32", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:56:36.283", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:56:36.283", "mi_style": null, "mi_prinx": 539}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574290.7870225868, 6222525.535934292]},"properties": {"vejnavn": "F. Vestergaards Gade ", "husnnr": "32", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:56:51.647", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:56:51.647", "mi_style": null, "mi_prinx": 540}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.7018685832, 6224472.544147842]},"properties": {"vejnavn": "Guldsmedegade", "husnnr": "26", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:58:17.377", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:58:17.377", "mi_style": null, "mi_prinx": 541}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574853.3419507187, 6224400.072895276]},"properties": {"vejnavn": "Klostergade ", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:01:19.687", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:46.85", "mi_style": null, "mi_prinx": 542}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574850.2618685832, 6224400.278234085]},"properties": {"vejnavn": "Klostergade", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:01:29.593", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:29.593", "mi_style": null, "mi_prinx": 543}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573800.5521852324, 6222947.219776177]},"properties": {"vejnavn": "Læssøesgade", "husnnr": "83", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:15:14.473", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:15:14.473", "mi_style": null, "mi_prinx": 544}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575433.1519507194, 6224837.509240246]},"properties": {"vejnavn": "Molsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:27:04.7", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:27:04.7", "mi_style": null, "mi_prinx": 545}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574117.4563655023, 6222280.277207391]},"properties": {"vejnavn": "Ordrupvej", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:27:44.68", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:27:44.68", "mi_style": null, "mi_prinx": 547}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573592.798767967, 6222723.202258727]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "17", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:30:01.607", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:01.607", "mi_style": null, "mi_prinx": 548}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573852.0671252563, 6224259.847022587]},"properties": {"vejnavn": "Ringkøbingvej ", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:31:29.06", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:31:29.06", "mi_style": null, "mi_prinx": 549}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573851.8617864472, 6224258.409650924]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:31:45.1", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:31:45.1", "mi_style": null, "mi_prinx": 550}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574375.1585652019, 6222033.415963625]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:33:45.17", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:45.17", "mi_style": null, "mi_prinx": 551}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574606.2883778228, 6227299.677618067]},"properties": {"vejnavn": "Vorregårds Allé ", "husnnr": "103", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:34:59.943", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:34:59.943", "mi_style": null, "mi_prinx": 552}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575306.5234312108, 6225834.736901434]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "13", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:36:31.937", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:36:31.937", "mi_style": null, "mi_prinx": 553}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575306.9341088288, 6225832.478174535]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "13", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:36:49.883", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:36:49.883", "mi_style": null, "mi_prinx": 554}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573236.9384143031, 6220161.223442913]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:38:24.073", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:38:24.073", "mi_style": null, "mi_prinx": 555}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570728.6893018486, 6216766.413757699]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:42:34.943", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:42:34.943", "mi_style": null, "mi_prinx": 556}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570725.4038809041, 6216767.645790554]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:42:46.957", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:42:46.957", "mi_style": null, "mi_prinx": 557}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570683.72010267, 6216786.947638603]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:20.413", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:20.413", "mi_style": null, "mi_prinx": 558}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570684.5414579061, 6216790.438398357]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:30.79", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:30.79", "mi_style": null, "mi_prinx": 559}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570716.1636344974, 6216831.916837782]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:47.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:47.467", "mi_style": null, "mi_prinx": 560}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570716.5743121156, 6216834.996919917]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:57.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:57.08", "mi_style": null, "mi_prinx": 561}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575070.7447433264, 6223252.195071867]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "144", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:45:13.057", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:45:13.057", "mi_style": null, "mi_prinx": 562}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574936.4408418891, 6222987.421971251]},"properties": {"vejnavn": "Marselisborg Allé", "husnnr": "30b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:46:52.93", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:46:52.93", "mi_style": null, "mi_prinx": 563}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573702.7326710331, 6224158.784515839]},"properties": {"vejnavn": "Regenburgs Plads", "husnnr": "50", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:48:19.28", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:48:19.28", "mi_style": null, "mi_prinx": 564}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573619.7098357283, 6224188.62422998]},"properties": {"vejnavn": "Regenburgsgade", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:50:10.157", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:50:10.157", "mi_style": null, "mi_prinx": 565}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573603.5732238201, 6222298.034907595]},"properties": {"vejnavn": "Rudpolh Wulffs Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:51:55.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:51:55.07", "mi_style": null, "mi_prinx": 566}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.381326311, 6223149.484703504]},"properties": {"vejnavn": "st st blichers gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:53:08.913", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:02:57.243", "mi_style": null, "mi_prinx": 567}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574499.2238193017, 6224391.241273099]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:58:48.823", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:58:48.823", "mi_style": null, "mi_prinx": 568}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575248.5108008216, 6223656.206817246]},"properties": {"vejnavn": "Toldbodgade", "husnnr": "8b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:31:29.01", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:31:29.01", "mi_style": null, "mi_prinx": 569}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575613.2119507189, 6225969.438398356]},"properties": {"vejnavn": "Herluf Trolles gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:35:59.73", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:35:59.73", "mi_style": null, "mi_prinx": 571}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575459.9619507184, 6225872.322381929]},"properties": {"vejnavn": "Otte Ruds Gade", "husnnr": "38", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:36:48.51", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:36:48.51", "mi_style": null, "mi_prinx": 572}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572973.5414579054, 6219822.767967144]},"properties": {"vejnavn": "Rundhøj Alle", "husnnr": "129", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:47:28.83", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:47:28.83", "mi_style": null, "mi_prinx": 573}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573964.5795687882, 6225531.675564681]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "84", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:52:36.853", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:52:36.853", "mi_style": null, "mi_prinx": 574}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573962.7315195069, 6225530.443531826]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "84", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:52:50.743", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:52:50.743", "mi_style": null, "mi_prinx": 575}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573763.6589431843, 6225400.732252075]},"properties": {"vejnavn": "Peter Fabers Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:02:57.817", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:02:57.817", "mi_style": null, "mi_prinx": 576}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573569.8198767971, 6225558.664271047]},"properties": {"vejnavn": "Peter Fabers Vej", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:03:59.61", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:03:59.61", "mi_style": null, "mi_prinx": 577}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573689.2009378455, 6225304.105604249]},"properties": {"vejnavn": "Viggo Stuckenbergs Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:05:26.077", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:05:26.077", "mi_style": null, "mi_prinx": 578}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571906.5431211507, 6223317.124229979]},"properties": {"vejnavn": "Hnerik Hertz Vej", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:06:25.737", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:06:25.737", "mi_style": null, "mi_prinx": 579}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572263.050246407, 6223168.713552361]},"properties": {"vejnavn": "O.I. offersens Vej", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:07:10.387", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:07:10.387", "mi_style": null, "mi_prinx": 580}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571635.578603885, 6220454.145748499]},"properties": {"vejnavn": "Grøndalsvej ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:06.363", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:06.363", "mi_style": null, "mi_prinx": 581}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571637.4266531663, 6220455.78845897]},"properties": {"vejnavn": "Grøndalsvej ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:17.097", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:17.097", "mi_style": null, "mi_prinx": 582}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571642.9708010103, 6220460.511251578]},"properties": {"vejnavn": "Grøndalsvej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:50.553", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:50.553", "mi_style": null, "mi_prinx": 583}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571645.8455443367, 6220461.948623242]},"properties": {"vejnavn": "Grøndalsvej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:11:00.837", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:11:00.837", "mi_style": null, "mi_prinx": 584}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575130.4157366346, 6223963.371970712]},"properties": {"vejnavn": "Fiskergade", "husnnr": "80", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:18:06.18", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:18:06.18", "mi_style": null, "mi_prinx": 585}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575127.8080963028, 6223964.838768398]},"properties": {"vejnavn": "Fiskergade", "husnnr": "80", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:18:18.71", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:18:18.71", "mi_style": null, "mi_prinx": 586}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575089.0245238632, 6223834.6749242265]},"properties": {"vejnavn": "Østergade", "husnnr": "48", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:19:11.287", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:19:11.287", "mi_style": null, "mi_prinx": 587}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574258.2222792607, 6222755.437371662]},"properties": {"vejnavn": "F. Vestergaards gade", "husnnr": "4", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:20:02.303", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:20:02.303", "mi_style": null, "mi_prinx": 588}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574263.5610882957, 6222709.852156057]},"properties": {"vejnavn": "F. Vestergaards Gade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:20:27.033", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:20:27.033", "mi_style": null, "mi_prinx": 589}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573703.2468583164, 6225533.67761807]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej ", "husnnr": "37", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 13:08:01.087", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 13:08:01.087", "mi_style": null, "mi_prinx": 590}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.4344056073, 6222520.606665064]},"properties": {"vejnavn": "Adoplh Meyers Vej", "husnnr": "10", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:06:23.613", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:06:23.613", "mi_style": null, "mi_prinx": 591}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573440.4595687881, 6220071.08932238]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:10:59.32", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:10:59.32", "mi_style": null, "mi_prinx": 592}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574108.964579056, 6219523.614989731]},"properties": {"vejnavn": "Niels Bjerres Vej", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:54:28.78", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:54:28.78", "mi_style": null, "mi_prinx": 593}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574773.7705954819, 6222620.240246407]},"properties": {"vejnavn": "Assensgade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 15:09:24.753", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 15:09:24.753", "mi_style": null, "mi_prinx": 594}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573563.520197629, 6225599.931539235]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "65", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-03-03 15:31:57.61", "rettet_af": "spatial_reader", "rettet_dato": "2025-03-03 15:31:57.61", "mi_style": null, "mi_prinx": 595}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573565.9366160922, 6225599.931539235]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "65", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-03-03 15:32:11.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-03-03 15:32:11.45", "mi_style": null, "mi_prinx": 596}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573900.4239732762, 6224444.639830079]},"properties": {"vejnavn": "Eugen Warmings Vej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 12:35:23.093", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:35:23.093", "mi_style": null, "mi_prinx": 597}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573895.0856880753, 6224474.736713882]},"properties": {"vejnavn": "Eugen Warmings Vej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 12:36:37.753", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:36:37.753", "mi_style": null, "mi_prinx": 598}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574904.906065573, 6222700.202576111]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 13:10:47.673", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 13:10:47.673", "mi_style": null, "mi_prinx": 599}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576675.4374502097, 6227137.355086146]},"properties": {"vejnavn": "stationsgade", "husnnr": "26", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 09:34:00.233", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:34:00.233", "mi_style": null, "mi_prinx": 600}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574471.4348262089, 6222940.82387054]},"properties": {"vejnavn": "Skt Anna Gade", "husnnr": null, "invalidepladser": null, "bemrk": "42", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 14:14:27.8", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 14:14:27.8", "mi_style": null, "mi_prinx": 601}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574477.5688115384, 6222914.615024131]},"properties": {"vejnavn": "Skt Anna Gade", "husnnr": "42", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 14:14:41.96", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 14:14:41.96", "mi_style": null, "mi_prinx": 602}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575230.8720257445, 6224047.548393872]},"properties": {"vejnavn": "Kannikegade 16", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 09:56:55.567", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:56:55.567", "mi_style": null, "mi_prinx": 603}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571983.2489361251, 6223205.13326127]},"properties": {"vejnavn": "Henrik Hertz Vej ", "husnnr": "72", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 10:59:53.147", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:59:53.147", "mi_style": null, "mi_prinx": 604}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571985.1224724249, 6223202.557148858]},"properties": {"vejnavn": "Henrik Hertz Vej", "husnnr": "72", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 11:01:03.507", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 11:01:03.507", "mi_style": null, "mi_prinx": 605}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574835.1320140511, 6222779.284543324]},"properties": {"vejnavn": "Hans Broges Gade", "husnnr": "17", "invalidepladser": 2, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 11:15:39.89", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 11:15:39.89", "mi_style": null, "mi_prinx": 606}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575250.902172792, 6224568.6143739]},"properties": {"vejnavn": "Studsgade ", "husnnr": "44", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 13:48:01.11", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 13:48:01.11", "mi_style": null, "mi_prinx": 607}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574412.2255140772, 6224274.489827803]},"properties": {"vejnavn": "Janus La Cours gade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:40:20.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:40:20.08", "mi_style": null, "mi_prinx": 608}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574548.0404080742, 6226415.892238429]},"properties": {"vejnavn": "Norges Allé ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:43:11.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:43:11.467", "mi_style": null, "mi_prinx": 609}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573738.9391065245, 6225498.3024416845]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:44:27.91", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:44:27.91", "mi_style": null, "mi_prinx": 610}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573740.2607020313, 6225496.834002232]},"properties": {"vejnavn": "Vilhelm Bergsøes vej", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:44:55.213", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:44:55.213", "mi_style": null, "mi_prinx": 611}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572487.2086247086, 6221123.172494171]},"properties": {"vejnavn": "Rørdamsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:47:21.347", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:47:21.347", "mi_style": null, "mi_prinx": 612}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572485.3438228437, 6221125.736596736]},"properties": {"vejnavn": "Rørdamsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:47:40.887", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:47:40.887", "mi_style": null, "mi_prinx": 613}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573450.7706904352, 6221551.299040821]},"properties": {"vejnavn": "H.C. Ørsteds Vej", "husnnr": "3b", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:49:03.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:49:03.467", "mi_style": null, "mi_prinx": 614}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574422.5215850822, 6227269.3811188815]},"properties": {"vejnavn": "Skrydstrupvej", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:50:15.83", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:50:15.83", "mi_style": null, "mi_prinx": 615}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574201.3469197123, 6222224.453960875]},"properties": {"vejnavn": "Ellemarksvej ", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:51:52.827", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:51:52.827", "mi_style": null, "mi_prinx": 616}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574261.9134668851, 6223538.282346269]},"properties": {"vejnavn": "Eckersbergsgade", "husnnr": "30", "invalidepladser": 1, "bemrk": "1345", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-12-02 13:14:47.253", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 13:14:47.253", "mi_style": null, "mi_prinx": 617}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572304.1753244308, 6223126.011642109]},"properties": {"vejnavn": "O. I. Offersensvej ", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 12:51:35.733", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 12:51:35.733", "mi_style": null, "mi_prinx": 618}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574370.9164150944, 6222348.226415094]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 14:21:24.417", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 14:21:24.417", "mi_style": null, "mi_prinx": 619}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574625.108490566, 6226140.433962264]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 14:22:33.083", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 14:22:33.083", "mi_style": null, "mi_prinx": 620}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575667.7509942062, 6225512.529924879]},"properties": {"vejnavn": "Trøjborgvej", "husnnr": "2b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-03 15:32:27.96", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-03 15:32:27.96", "mi_style": null, "mi_prinx": 621}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575717.5020655771, 6225812.445111568]},"properties": {"vejnavn": "Otte Ruds Gade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-10 15:36:13.387", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-10 15:36:13.387", "mi_style": null, "mi_prinx": 622}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574461.6616818061, 6226572.319186298]},"properties": {"vejnavn": "Stockholmsgade", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-10 16:10:04.953", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-10 16:10:04.953", "mi_style": null, "mi_prinx": 623}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575466.6230287044, 6226492.72476525]},"properties": {"vejnavn": "Skovfaldet", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-24 13:49:38.32", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-24 13:49:38.32", "mi_style": null, "mi_prinx": 624}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574060.9656603774, 6222480.358490566]},"properties": {"vejnavn": "Hans Schourups Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-04-14 13:45:00.16", "rettet_af": "spatial_reader", "rettet_dato": "2026-04-14 13:45:00.16", "mi_style": null, "mi_prinx": 626}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574258.8724698942, 6211072.413328694]},"properties": {"vejnavn": "Dampmøllevej", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:50:52.313", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:50:52.313", "mi_style": null, "mi_prinx": 628}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574259.8084374957, 6211070.354199971]},"properties": {"vejnavn": "Dampmøllevej ", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:51:13.877", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:51:13.877", "mi_style": null, "mi_prinx": 629}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572717.6960377358, 6224879.641509433]},"properties": {"vejnavn": "Præstevamgsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:52:51.823", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:52:51.823", "mi_style": null, "mi_prinx": 630}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573286.2358490566, 6225834.424528301]},"properties": {"vejnavn": "Majdalen", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:54:06.53", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:54:06.53", "mi_style": null, "mi_prinx": 631}}]} From 8758fe57b44a02df4b23a0852894e1e728979d18 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 14 Sep 2026 13:30:02 +0200 Subject: [PATCH 086/101] Added test controller --- src/Controller/TestController.php | 78 +++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/Controller/TestController.php diff --git a/src/Controller/TestController.php b/src/Controller/TestController.php new file mode 100644 index 0000000..30ec078 --- /dev/null +++ b/src/Controller/TestController.php @@ -0,0 +1,78 @@ + Requirement::CATCH_ALL], methods: [Request::METHOD_GET], priority: -99)] + public function index(?string $path = null): Response + { + return $this->render(null === $path ? 'test/index.html.twig' : sprintf('test/%s.html.twig', $path)); + } + + #[Route( + path: '/data/{path}.{_format}', + methods: [Request::METHOD_GET], + requirements: [ + 'path' => Requirement::CATCH_ALL, + '_format' => 'json|geojson', + ], + defaults: ['_format' => self::FORMAT_JSON], + priority: -98, + )] + public function data(Request $request, string $path, string $_format): Response + { + // Remove "/test/" + $path = substr($request->getRequestUri(), 6); + $path = realpath(__DIR__.'/../../tests/resources/'.$path); + if (!file_exists($path)) { + throw new NotFoundHttpException($path); + } + + $contentType = match ($_format) { + self::FORMAT_GEOJSON => self::APPLICATION_GEOJSON, + default => self::APPLICATION_JSON, + }; + + return new BinaryFileResponse($path, headers: [ + 'content-type' => $contentType, + ]); + } + + #[Route('/config', name: 'config', methods: [Request::METHOD_GET])] + public function config( + #[MapQueryParameter('type')] + string $type, + ): JsonResponse { + $configName = match ($type) { + 'https://smartdatamodels.org/dataModel.Parking/OnStreetParking' => 'Parking/OnStreetParking', + default => throw new BadRequestHttpException('Invalid type'), + }; + + $data = Yaml::parseFile(__DIR__.'/../../tests/resources/config/'.$configName.'.yaml'); + + return new JsonResponse($data); + } +} From ee308e9587b2fe10204f7b915f4d9a5fb8f114f7 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 14 Sep 2026 13:42:35 +0200 Subject: [PATCH 087/101] Moved test source --- src/{Source/Test => Test/Source}/TestHandicapParking.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{Source/Test => Test/Source}/TestHandicapParking.php (99%) diff --git a/src/Source/Test/TestHandicapParking.php b/src/Test/Source/TestHandicapParking.php similarity index 99% rename from src/Source/Test/TestHandicapParking.php rename to src/Test/Source/TestHandicapParking.php index fe886bc..2f9fd5c 100644 --- a/src/Source/Test/TestHandicapParking.php +++ b/src/Test/Source/TestHandicapParking.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Source\Test; +namespace App\Test\Source; use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; From 255f0698e0a5c2d74ba208847bc785f59fd2c900 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 14 Sep 2026 15:39:49 +0200 Subject: [PATCH 088/101] Refactored test source stuff --- docs/Testing.md | 35 +- src/Command/BrokerEntityDelete.php | 5 + .../Command/SourceFetchContentCommand.php | 57 + src/Test/Command/SourcesImportCommand.php | 54 + .../TestHandicapParking.php | 57 +- src/Test/Source/Osm/TestHandicapParking.php | 235 + tests/resources/README.md | 4 +- tests/resources/data/fetch-resources | 17 - .../api/interpreter?osm-handicap-parking | 18848 ++++++++++++++++ ...atialmap?mtm_spatialmaps-handicap-parking} | 2 +- 10 files changed, 19263 insertions(+), 51 deletions(-) create mode 100644 src/Test/Command/SourceFetchContentCommand.php create mode 100644 src/Test/Command/SourcesImportCommand.php rename src/Test/Source/{ => MtmSpatialMaps}/TestHandicapParking.php (54%) create mode 100644 src/Test/Source/Osm/TestHandicapParking.php delete mode 100755 tests/resources/data/fetch-resources create mode 100644 tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking rename tests/resources/data/webkort.aarhuskommune.dk/{spatialmap?page=get_geojson_opendata&datasource=invap => spatialmap?mtm_spatialmaps-handicap-parking} (99%) diff --git a/docs/Testing.md b/docs/Testing.md index 9c66a2f..ee1adf7 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -1,12 +1,43 @@ # Testing +## Test sources + For local testing and development we use a test controller that's only enabled in the `dev` and `test` environments. -Load some local test data from a test data source (only available in the `dev` and `test` environments): +Furthermore, we use static test sources (fetching locally stored data) for testing and development. By convention, +the ID of a test source starts with `test:`, i.e. they can be listed by running + +```shell +docker compose exec phpfpm php bin/console app:source:list | grep 'test:' +``` + +Example: Import and show data from the test source `test:mtm_spatialmaps-handicap-parking`: ```shell docker compose exec phpfpm php bin/console app:source:import test:mtm_spatialmaps-handicap-parking -curl 'http://enter.local.itkdev.dk/data/ngsi-ld/v1/entities?type=https://smartdatamodels.org/dataModel.Parking/OnStreetParking' +docker compose exec phpfpm curl 'http://scorpio:9090/ngsi-ld/v1/entities?type=https://smartdatamodels.org/dataModel.Parking/OnStreetParking' ``` See the result on . + +### Refreshing test source data + +The data for test sources are stored as plain files in the [../tests/resources/data](../tests/resources/data) folder. + +The data files can be updated by running + +```shell +docker compose exec phpfpm php bin/console test:source:fetch-content +``` + +Load *all test sources* with + +```shell +docker compose exec phpfpm php bin/console test:source:import +``` + +To empty your local broker, e.g. before loading test data, run + +```shell +docker compose exec phpfpm php bin/console app:broker:entity:delete --all +``` diff --git a/src/Command/BrokerEntityDelete.php b/src/Command/BrokerEntityDelete.php index 3e585cf..9fd7c9c 100644 --- a/src/Command/BrokerEntityDelete.php +++ b/src/Command/BrokerEntityDelete.php @@ -6,6 +6,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -30,6 +31,10 @@ public function __invoke( if ($all) { $data = $brokerClient->request(Request::METHOD_GET, '/ngsi-ld/v1/types')->toArray(); $entityTypes = $data['typeList'] ?? []; + } else { + if (0 === count($entityTypes)) { + throw new RuntimeException('Missing entity types'); + } } $limit = 1000; diff --git a/src/Test/Command/SourceFetchContentCommand.php b/src/Test/Command/SourceFetchContentCommand.php new file mode 100644 index 0000000..2e823eb --- /dev/null +++ b/src/Test/Command/SourceFetchContentCommand.php @@ -0,0 +1,57 @@ +getSources(), static fn (SourceInterface $source) => str_starts_with($source->definition->id, 'test:')); + foreach ($testSources as $source) { + try { + $io->section($source); + $url = $source->definition->landingPage; + $filename = preg_replace('@^[a-z]+://[^/]+/test/@', '', $source->definition->accessUrl); + $filename = __DIR__.'/../../../tests/resources/'.$filename; + + if ($filesystem->exists($filename)) { + $filesystem->remove($filename); + } + + $io->writeln(sprintf('Fetching "%s"', $url)); + $response = $httpClient->request(Request::METHOD_GET, $url); + $content = $response->getContent(); + $filesystem->dumpFile($filename, $content); + $io->success(sprintf('Content written to file %s', realpath($filename))); + } catch (\Exception $e) { + $io->error($e->getMessage()); + } + } + + return Command::SUCCESS; + } +} diff --git a/src/Test/Command/SourcesImportCommand.php b/src/Test/Command/SourcesImportCommand.php new file mode 100644 index 0000000..2d2715e --- /dev/null +++ b/src/Test/Command/SourcesImportCommand.php @@ -0,0 +1,54 @@ +getSources(), static fn (SourceInterface $source) => str_starts_with($source->definition->id, 'test:')); + foreach ($testSources as $source) { + try { + $io->section($source); + $input = new ArrayInput([ + 'command' => 'app:source:import', + 'source' => $source->definition->id, + ]); + $input->setInteractive(false); + + $application->doRun($input, $output); + } catch (\Exception $e) { + $io->error($e->getMessage()); + } + } + + return Command::SUCCESS; + } +} diff --git a/src/Test/Source/TestHandicapParking.php b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php similarity index 54% rename from src/Test/Source/TestHandicapParking.php rename to src/Test/Source/MtmSpatialMaps/TestHandicapParking.php index 2f9fd5c..86dd040 100644 --- a/src/Test/Source/TestHandicapParking.php +++ b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Test\Source; +namespace App\Test\Source\MtmSpatialMaps; use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; @@ -15,37 +15,36 @@ */ #[When('dev')] #[When('test')] -final readonly class TestHandicapParking extends AbstractSource +#[Definition( + // By convention the ID as a test source must start with `test:` + id: 'test:mtm_spatialmaps-handicap-parking', + title: 'Test: Handicapparkering, Aarhus Kommune', + description: '', + publisher: '', + contact: '', + // The landingPage URL is used by the `test:source:fetch-content` command to fetch test content. + landingPage: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', + accessUrl: 'http://nginx:8080/test/data/webkort.aarhuskommune.dk/spatialmap?mtm_spatialmaps-handicap-parking', + mediaType: 'application/geo+json', + crs: 'EPSG:25832', + model: 'OnStreetParking', + contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', + updateFrequency: '', + licence: null, + omittedFields: [ + 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', + 'oprettet_af' => 'Directory username of the municipal employee who created the record.', + 'rettet_af' => 'Directory username of the municipal employee who last edited the record.', + 'oprettet_dato' => 'Describes the register record.', + 'rettet_dato' => 'Describes the register record.', + 'mi_style' => 'MapInfo rendering style, empty throughout the export.', + ], +)] +final class TestHandicapParking extends AbstractSource { - public Definition $definition; - public function __construct( - private Wgs84Transformer $transformer, + private readonly Wgs84Transformer $transformer, ) { - $this->definition = new Definition( - id: 'test:mtm_spatialmaps-handicap-parking', - title: 'Test: Handicapparkering, Aarhus Kommune', - description: 'Disabled parking bays in Aarhus Municipality, with the number of reserved bays per location.', - publisher: 'Aarhus Kommune', - contact: 'ppg@aarhus.dk', - landingPage: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune', - accessUrl: 'http://nginx:8080/test/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', - mediaType: 'application/geo+json', - crs: 'EPSG:25832', - model: 'OnStreetParking', - contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', - updateFrequency: 'continuous', - licence: null, - - omittedFields: [ - 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', - 'oprettet_af' => 'Directory username of the municipal employee who created the record.', - 'rettet_af' => 'Directory username of the municipal employee who last edited the record.', - 'oprettet_dato' => 'Describes the register record.', - 'rettet_dato' => 'Describes the register record.', - 'mi_style' => 'MapInfo rendering style, empty throughout the export.', - ], - ); } /** diff --git a/src/Test/Source/Osm/TestHandicapParking.php b/src/Test/Source/Osm/TestHandicapParking.php new file mode 100644 index 0000000..48a823c --- /dev/null +++ b/src/Test/Source/Osm/TestHandicapParking.php @@ -0,0 +1,235 @@ + 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', + 'capacity' => 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.', + 'parking' => 'Facility siting (street_side, surface, underground); every record is published under the one model this source names.', + 'orientation' => 'How bays lie relative to the road; the model has no counterpart.', + 'disabled' => 'Access restriction on street-side parking; redundant with the category every entity is published with.', + 'access' => 'Who may enter; mapping it onto permit attributes needs an interpretation the tag values do not support.', + 'fee:conditional' => 'Time-qualified refinement of fee; the category values the plain fee tag maps onto carry no schedule.', + 'surface' => 'Paving material; the model has no counterpart. 6% of records carry it.', + 'wheelchair' => 'Step-free access to the place, not the parking capacity. 3% of records carry it.', + 'capacity:charging' => 'Bays with charging points; a different subset than the reserved bays this data set publishes. 2% of records carry it.', + 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', + 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', + ], +)] +final class TestHandicapParking extends AbstractSource +{ + public function __construct( + private readonly Wgs84Transformer $transformer, + ) { + } + + /** + * Maps one feed record onto an NgsiEntity. + * + * @param array $data Overpass JSON element + */ + public function createNgsiEntity(array $data): ?NgsiEntity + { + $type = $data['type'] ?? null; + $id = $data['id'] ?? null; + + // OSM ids are only unique per element type, so both are needed to + // address the same object again on the next import. + if (!\is_string($type) || !\is_int($id)) { + return null; + } + + $geometry = $this->geometry($data); + if (null === $geometry) { + return null; + } + + $tags = \is_array($data['tags'] ?? null) ? $data['tags'] : []; + + $entity = new NgsiEntity( + \sprintf('urn:ngsi-ld:%s:aarhus-handicap-osm-%s-%d', $this->definition->model, $type, $id), + $this->definition->model + ); + + return $entity + ->setProperty('name', trim((string) ($tags['name'] ?? ''))) + ->setProperty('description', trim((string) ($tags['description'] ?? ''))) + ->setProperty('category', $this->category($tags)) + ->setProperty('totalSpotNumber', $this->reservedBays($tags)) + ->setProperty('source', $this->definition->accessUrl) + ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); + } + + /** + * Every record is disabled parking; the fee tag refines that with the + * model's charging categories. Only its two plain values map — an + * untagged or unrecognised value states nothing about charging rather + * than assuming free. + * + * @param array $tags + * + * @return list + */ + private function category(array $tags): array + { + return match ($tags['fee'] ?? null) { + 'yes' => ['forDisabled', 'feeCharged'], + 'no' => ['forDisabled', 'free'], + default => ['forDisabled'], + }; + } + + /** + * Number of reserved bays the record carries. + * + * capacity:disabled counts them directly whatever the record is. A single + * bay (parking_space=disabled) is reserved in its entirety, so its own + * capacity applies — one when untagged, per the tag's definition. A + * facility's plain capacity counts all its bays and is never used, and + * capacity:disabled=yes states that reserved bays exist without counting + * them, so nothing is published for it. + * + * @param array $tags + */ + private function reservedBays(array $tags): ?int + { + if (null !== $count = $this->count($tags, 'capacity:disabled')) { + return $count; + } + + if ('disabled' === ($tags['parking_space'] ?? null)) { + return $this->count($tags, 'capacity') ?? 1; + } + + return null; + } + + /** + * @param array $tags + */ + private function count(array $tags, string $tag): ?int + { + $value = $tags[$tag] ?? null; + + return \is_string($value) && ctype_digit($value) ? (int) $value : null; + } + + /** + * @param array $element + * + * @return array{type: string, coordinates: mixed}|null + */ + private function geometry(array $element): ?array + { + return match ($element['type'] ?? null) { + 'node' => $this->point($element['lon'] ?? null, $element['lat'] ?? null), + 'way' => $this->wayGeometry($element['geometry'] ?? null), + // The feed's output mode carries no member geometry for + // relations, only their bounding box, so the centre of that box + // is the best location available. + 'relation' => $this->boundsCentre($element['bounds'] ?? null), + default => null, + }; + } + + /** + * @return array{type: string, coordinates: array{float, float}}|null + */ + private function point(mixed $longitude, mixed $latitude): ?array + { + if (!is_numeric($longitude) || !is_numeric($latitude)) { + return null; + } + + return ['type' => 'Point', 'coordinates' => [(float) $longitude, (float) $latitude]]; + } + + /** + * @return array{type: string, coordinates: mixed}|null + */ + private function wayGeometry(mixed $vertices): ?array + { + if (!\is_array($vertices)) { + return null; + } + + $positions = []; + foreach ($vertices as $vertex) { + if (!\is_array($vertex) || !is_numeric($vertex['lon'] ?? null) || !is_numeric($vertex['lat'] ?? null)) { + return null; + } + + $positions[] = [(float) $vertex['lon'], (float) $vertex['lat']]; + } + + // A way that returns to its first vertex outlines an area — here a + // bay or a parking lot — so it becomes a Polygon ring rather than a + // line along its edge. Four positions are a ring's minimum: three + // corners plus the repeated first. + if (\count($positions) >= 4 && $positions[0] === $positions[array_key_last($positions)]) { + return ['type' => 'Polygon', 'coordinates' => [$positions]]; + } + + if (\count($positions) >= 2) { + return ['type' => 'LineString', 'coordinates' => $positions]; + } + + return null; + } + + /** + * @return array{type: string, coordinates: array{float, float}}|null + */ + private function boundsCentre(mixed $bounds): ?array + { + if (!\is_array($bounds)) { + return null; + } + + foreach (['minlon', 'minlat', 'maxlon', 'maxlat'] as $edge) { + if (!is_numeric($bounds[$edge] ?? null)) { + return null; + } + } + + return $this->point( + ((float) $bounds['minlon'] + (float) $bounds['maxlon']) / 2, + ((float) $bounds['minlat'] + (float) $bounds['maxlat']) / 2 + ); + } +} diff --git a/tests/resources/README.md b/tests/resources/README.md index 63a8c9b..060124b 100644 --- a/tests/resources/README.md +++ b/tests/resources/README.md @@ -3,7 +3,7 @@ Run ```shell -./data/fetch-resources +docker compose exec phpfpm php bin/console test:source-content:fetch ``` -to update the data resources. +to update the test data resources. diff --git a/tests/resources/data/fetch-resources b/tests/resources/data/fetch-resources deleted file mode 100755 index 77b20c9..0000000 --- a/tests/resources/data/fetch-resources +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -o errtrace -o noclobber -o nounset -o pipefail -IFS=$'\n\t' - -script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) - -cd "$script_dir" - -urls=( - "https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap" -) -for url in "${urls[@]}"; do - target_file_name="${url##https://}" - mkdir -p "$(dirname "$target_file_name")" - curl --silent "$url" >| "$target_file_name" - echo "$url → $target_file_name" -done diff --git a/tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking b/tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking new file mode 100644 index 0000000..5bfeb7f --- /dev/null +++ b/tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking @@ -0,0 +1,18848 @@ +{ + "version": 0.6, + "generator": "Overpass API 0.7.62.11 87bfad18", + "osm3s": { + "timestamp_osm_base": "2026-09-14T13:37:35Z", + "timestamp_areas_base": "2026-09-13T09:23:35Z", + "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL." + }, + "elements": [ + +{ + "type": "node", + "id": 3580886094, + "lat": 56.1540563, + "lon": 10.2141175, + "tags": { + "access": "yes", + "amenity": "parking", + "capacity:disabled": "4", + "fee": "yes", + "parking": "surface" + } +}, +{ + "type": "node", + "id": 8134288883, + "lat": 56.1880475, + "lon": 10.1724790, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134288884, + "lat": 56.1880635, + "lon": 10.1724327, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134288885, + "lat": 56.1880835, + "lon": 10.1723902, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134288886, + "lat": 56.1881235, + "lon": 10.1723425, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134288887, + "lat": 56.1881507, + "lon": 10.1722965, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134288897, + "lat": 56.1885207, + "lon": 10.1713835, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134298246, + "lat": 56.1884511, + "lon": 10.1714340, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134298264, + "lat": 56.1886537, + "lon": 10.1717005, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8134298267, + "lat": 56.1885890, + "lon": 10.1716971, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050455, + "lat": 56.1888332, + "lon": 10.1690964, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050456, + "lat": 56.1888028, + "lon": 10.1691129, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050457, + "lat": 56.1887709, + "lon": 10.1691301, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050458, + "lat": 56.1886695, + "lon": 10.1690153, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050459, + "lat": 56.1886304, + "lon": 10.1690332, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050460, + "lat": 56.1886619, + "lon": 10.1692197, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050461, + "lat": 56.1886332, + "lon": 10.1692384, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050462, + "lat": 56.1886341, + "lon": 10.1684121, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050463, + "lat": 56.1886332, + "lon": 10.1684769, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050464, + "lat": 56.1883481, + "lon": 10.1688661, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050465, + "lat": 56.1883500, + "lon": 10.1689293, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050466, + "lat": 56.1882130, + "lon": 10.1690623, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050467, + "lat": 56.1882167, + "lon": 10.1691222, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050468, + "lat": 56.1896107, + "lon": 10.1659157, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050469, + "lat": 56.1895942, + "lon": 10.1659846, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 8983050470, + "lat": 56.1895662, + "lon": 10.1659138, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 9322358023, + "lat": 56.1577621, + "lon": 10.1916218, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 9700854897, + "lat": 56.1999829, + "lon": 10.2237171, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 9727605404, + "lat": 56.1548474, + "lon": 10.1823601, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10121348257, + "lat": 56.1587281, + "lon": 10.2050798, + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10121348258, + "lat": 56.1587233, + "lon": 10.2051616, + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10687920010, + "lat": 56.1509931, + "lon": 10.1637614, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10965058334, + "lat": 56.1600949, + "lon": 10.1971488, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10966056821, + "lat": 56.1672444, + "lon": 10.2032256, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10966089271, + "lat": 56.0884662, + "lon": 10.2215781, + "tags": { + "amenity": "parking_space", + "capacity": "4", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10966089272, + "lat": 56.0883070, + "lon": 10.2212489, + "tags": { + "amenity": "parking_space", + "capacity": "4", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10967376053, + "lat": 56.1656118, + "lon": 10.2027526, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10967380941, + "lat": 56.1555258, + "lon": 10.2110997, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10967409246, + "lat": 56.1544680, + "lon": 10.2005485, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10967938203, + "lat": 56.1375208, + "lon": 10.1967317, + "tags": { + "amenity": "parking_space", + "capacity": "3", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10975000722, + "lat": 56.1523509, + "lon": 10.1985042, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10975000723, + "lat": 56.1523399, + "lon": 10.1985996, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10976078410, + "lat": 56.2043522, + "lon": 10.1789339, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 10976078411, + "lat": 56.2045499, + "lon": 10.1787763, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 11249239926, + "lat": 56.1526083, + "lon": 10.1951472, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 12368170867, + "lat": 56.1676256, + "lon": 10.2255202, + "tags": { + "amenity": "parking", + "brand": "Q-Park", + "brand:wikidata": "Q1127798", + "capacity": "299", + "capacity:disabled": "3", + "fee": "yes", + "layer": "-1", + "name": "Q-Park SHIP", + "operator": "Q-Park", + "operator:type": "private", + "operator:wikidata": "Q1127798", + "parking": "underground" + } +}, +{ + "type": "node", + "id": 12368170868, + "lat": 56.1664403, + "lon": 10.2264836, + "tags": { + "amenity": "parking", + "brand": "Q-Park", + "brand:wikidata": "Q1127798", + "capacity": "307", + "capacity:disabled": "4", + "fee": "yes", + "layer": "-1", + "name": "Q-Park Z-huset", + "operator": "Q-Park", + "operator:type": "private", + "operator:wikidata": "Q1127798", + "parking": "underground" + } +}, +{ + "type": "node", + "id": 12368279328, + "lat": 56.1661910, + "lon": 10.2294715, + "tags": { + "amenity": "parking", + "brand": "Q-Park", + "brand:wikidata": "Q1127798", + "capacity": "574", + "capacity:disabled": "3", + "fee": "yes", + "layer": "-1", + "name": "Q-Park Lighthouse", + "operator": "Q-Park", + "operator:type": "private", + "operator:wikidata": "Q1127798", + "parking": "underground" + } +}, +{ + "type": "node", + "id": 13767419249, + "lat": 56.1451174, + "lon": 10.1951941, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 13781618716, + "lat": 56.1294935, + "lon": 10.2028416, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 13781761916, + "lat": 56.1220894, + "lon": 10.1696146, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 13786925366, + "lat": 56.1403121, + "lon": 10.1924982, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 13928234157, + "lat": 56.1424644, + "lon": 10.2326602, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 13928234158, + "lat": 56.1424842, + "lon": 10.2326313, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 13987835028, + "lat": 56.0378975, + "lon": 10.0846591, + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 13987835029, + "lat": 56.0379577, + "lon": 10.0846883, + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14088025059, + "lat": 56.1168619, + "lon": 10.1540999, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled", + "surface": "asphalt" + } +}, +{ + "type": "node", + "id": 14088025060, + "lat": 56.1168873, + "lon": 10.1540407, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled", + "surface": "asphalt" + } +}, +{ + "type": "node", + "id": 14158118867, + "lat": 56.2817983, + "lon": 10.3260199, + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14158118868, + "lat": 56.2817442, + "lon": 10.3269738, + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14158118869, + "lat": 56.2805578, + "lon": 10.3258027, + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169686176, + "lat": 56.1679970, + "lon": 10.2186603, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169686177, + "lat": 56.1680403, + "lon": 10.2186824, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169686178, + "lat": 56.1680978, + "lon": 10.2187051, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169687624, + "lat": 56.1661885, + "lon": 10.2263206, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169687625, + "lat": 56.1663363, + "lon": 10.2261417, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169687626, + "lat": 56.1663940, + "lon": 10.2260702, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169687627, + "lat": 56.1664476, + "lon": 10.2260070, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169687628, + "lat": 56.1665276, + "lon": 10.2261316, + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169687629, + "lat": 56.1663168, + "lon": 10.2263852, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169787820, + "lat": 56.1472202, + "lon": 10.2040724, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169787821, + "lat": 56.1472295, + "lon": 10.2042905, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169803563, + "lat": 56.1434892, + "lon": 10.2015841, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169807689, + "lat": 56.1434863, + "lon": 10.2064821, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169821681, + "lat": 56.1424807, + "lon": 10.2057009, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169830204, + "lat": 56.1434497, + "lon": 10.2055057, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169830412, + "lat": 56.1427391, + "lon": 10.2030914, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169830413, + "lat": 56.1485083, + "lon": 10.2058145, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169831556, + "lat": 56.1438953, + "lon": 10.2054667, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169831591, + "lat": 56.1422967, + "lon": 10.2026576, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169831596, + "lat": 56.1423460, + "lon": 10.2025848, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169831597, + "lat": 56.1421623, + "lon": 10.2028179, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169832889, + "lat": 56.1440572, + "lon": 10.2068511, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169836572, + "lat": 56.1413079, + "lon": 10.2069535, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169841585, + "lat": 56.1442714, + "lon": 10.2028588, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169850040, + "lat": 56.1491450, + "lon": 10.2059184, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169850041, + "lat": 56.1491816, + "lon": 10.2059338, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169850078, + "lat": 56.1597507, + "lon": 10.1998418, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169850079, + "lat": 56.1588765, + "lon": 10.1994326, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169850080, + "lat": 56.1586300, + "lon": 10.1994415, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169904797, + "lat": 56.1743624, + "lon": 10.2183061, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169906806, + "lat": 56.1744615, + "lon": 10.2018153, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169906925, + "lat": 56.1731554, + "lon": 10.2087840, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169906926, + "lat": 56.1731096, + "lon": 10.2092138, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169908644, + "lat": 56.1621777, + "lon": 10.2136222, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169908828, + "lat": 56.1761755, + "lon": 10.2052430, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169908838, + "lat": 56.1588155, + "lon": 10.2039459, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169909652, + "lat": 56.1720058, + "lon": 10.2136745, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169911249, + "lat": 56.1712632, + "lon": 10.2194550, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169913091, + "lat": 56.1734278, + "lon": 10.2187420, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169913092, + "lat": 56.1733982, + "lon": 10.2189856, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169913269, + "lat": 56.1697640, + "lon": 10.2177351, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169913903, + "lat": 56.1718591, + "lon": 10.2153237, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169916860, + "lat": 56.1724696, + "lon": 10.2143630, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169920887, + "lat": 56.1728618, + "lon": 10.2161748, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169923639, + "lat": 56.1733585, + "lon": 10.2153221, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169923640, + "lat": 56.1727133, + "lon": 10.2179097, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169932003, + "lat": 56.1625591, + "lon": 10.2146187, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169932513, + "lat": 56.1585746, + "lon": 10.2029894, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169933685, + "lat": 56.1619060, + "lon": 10.2154344, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169933686, + "lat": 56.1613047, + "lon": 10.2055519, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169938117, + "lat": 56.1629003, + "lon": 10.2095334, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169938160, + "lat": 56.1599392, + "lon": 10.1985971, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169939005, + "lat": 56.1604626, + "lon": 10.2001210, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169939006, + "lat": 56.1590129, + "lon": 10.1985816, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169939007, + "lat": 56.1589115, + "lon": 10.1987599, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169940004, + "lat": 56.1593884, + "lon": 10.2042796, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169944488, + "lat": 56.1599590, + "lon": 10.1999475, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14169949248, + "lat": 56.1610291, + "lon": 10.1976031, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170333137, + "lat": 56.1755016, + "lon": 10.1956990, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170336776, + "lat": 56.1476389, + "lon": 10.1943080, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170346551, + "lat": 56.1528856, + "lon": 10.2113361, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170350721, + "lat": 56.1783064, + "lon": 10.1994971, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170350722, + "lat": 56.1711032, + "lon": 10.1928484, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170367029, + "lat": 56.1631267, + "lon": 10.2018958, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170371747, + "lat": 56.1628985, + "lon": 10.2011554, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170372770, + "lat": 56.1470938, + "lon": 10.2001516, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170372771, + "lat": 56.1482967, + "lon": 10.1989900, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170373960, + "lat": 56.1482552, + "lon": 10.2001790, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170380206, + "lat": 56.1674221, + "lon": 10.2058099, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170380213, + "lat": 56.1470603, + "lon": 10.1962730, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170394905, + "lat": 56.1668487, + "lon": 10.2064353, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170394906, + "lat": 56.1558461, + "lon": 10.2077463, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170396339, + "lat": 56.1478549, + "lon": 10.1998957, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170405754, + "lat": 56.1482031, + "lon": 10.1933606, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170405755, + "lat": 56.1482802, + "lon": 10.1933249, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14170409170, + "lat": 56.1479089, + "lon": 10.1974649, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172831091, + "lat": 56.1432391, + "lon": 10.1929917, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172831092, + "lat": 56.1405817, + "lon": 10.1924571, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172831355, + "lat": 56.1418549, + "lon": 10.1956314, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172831356, + "lat": 56.1419896, + "lon": 10.1955927, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172835623, + "lat": 56.1408481, + "lon": 10.1968133, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172850106, + "lat": 56.1443757, + "lon": 10.1880379, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172850107, + "lat": 56.1442731, + "lon": 10.1880681, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172850108, + "lat": 56.1441991, + "lon": 10.1879344, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172850109, + "lat": 56.1452313, + "lon": 10.1865095, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172851407, + "lat": 56.1438456, + "lon": 10.1940359, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172859114, + "lat": 56.1440356, + "lon": 10.1951270, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172859115, + "lat": 56.1395404, + "lon": 10.1927862, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172862733, + "lat": 56.1400411, + "lon": 10.2002248, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172864647, + "lat": 56.1456654, + "lon": 10.1986113, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172864648, + "lat": 56.1454777, + "lon": 10.1986760, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172868358, + "lat": 56.1431324, + "lon": 10.1940599, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172868359, + "lat": 56.1425491, + "lon": 10.1930269, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172871119, + "lat": 56.1388167, + "lon": 10.1896039, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172874527, + "lat": 56.1433302, + "lon": 10.1928363, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172874528, + "lat": 56.1431812, + "lon": 10.1922351, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172874529, + "lat": 56.1418192, + "lon": 10.1944963, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172880968, + "lat": 56.1426382, + "lon": 10.1888597, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172887605, + "lat": 56.1456476, + "lon": 10.1873544, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14172976646, + "lat": 56.1459357, + "lon": 10.1912440, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173004585, + "lat": 56.1465830, + "lon": 10.1926844, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173006952, + "lat": 56.1477871, + "lon": 10.1920433, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173007961, + "lat": 56.1439103, + "lon": 10.1844770, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173007985, + "lat": 56.1579200, + "lon": 10.1949427, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173007986, + "lat": 56.1580667, + "lon": 10.1948478, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173013256, + "lat": 56.1548724, + "lon": 10.1955123, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173013265, + "lat": 56.1544749, + "lon": 10.1911284, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173014436, + "lat": 56.1673068, + "lon": 10.1848612, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173022370, + "lat": 56.1437067, + "lon": 10.1852890, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173025674, + "lat": 56.1568221, + "lon": 10.1888599, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173026047, + "lat": 56.1503899, + "lon": 10.1952918, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173026048, + "lat": 56.1502241, + "lon": 10.1958906, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173026049, + "lat": 56.1503498, + "lon": 10.1959194, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173026629, + "lat": 56.1452248, + "lon": 10.1922988, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173026630, + "lat": 56.1505562, + "lon": 10.1973576, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173026631, + "lat": 56.1553302, + "lon": 10.1958364, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173028204, + "lat": 56.1462905, + "lon": 10.1886344, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173029608, + "lat": 56.1508613, + "lon": 10.1961515, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173030782, + "lat": 56.1334000, + "lon": 10.1820410, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173032304, + "lat": 56.1583838, + "lon": 10.1891436, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173032305, + "lat": 56.1585777, + "lon": 10.1892948, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173032306, + "lat": 56.1587265, + "lon": 10.1894361, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173034017, + "lat": 56.1551091, + "lon": 10.1987122, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173040110, + "lat": 56.1568290, + "lon": 10.1865189, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173041825, + "lat": 56.1679644, + "lon": 10.1891946, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173058756, + "lat": 56.1739032, + "lon": 10.1804173, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173063999, + "lat": 56.1848356, + "lon": 10.2019531, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173068543, + "lat": 56.0465039, + "lon": 10.1915405, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173085095, + "lat": 56.0464185, + "lon": 10.1928637, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173086863, + "lat": 56.1150473, + "lon": 10.1917452, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173115399, + "lat": 56.1614838, + "lon": 10.1337963, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173116645, + "lat": 56.1522093, + "lon": 10.1675012, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173120355, + "lat": 56.1480892, + "lon": 10.1632451, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173123010, + "lat": 56.1297274, + "lon": 10.1660388, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173124301, + "lat": 56.1494381, + "lon": 10.1574841, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173607997, + "lat": 56.1436201, + "lon": 10.1952234, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173714658, + "lat": 56.1563976, + "lon": 10.2034076, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173868701, + "lat": 56.1435920, + "lon": 10.1964848, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173983662, + "lat": 56.1553722, + "lon": 10.1942577, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14173983714, + "lat": 56.1576418, + "lon": 10.1980342, + "tags": { + "access": "yes", + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "node", + "id": 14175440219, + "lat": 56.1500985, + "lon": 10.1956761, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 23858719, + "bounds": { + "minlat": 56.1752108, + "minlon": 10.0655325, + "maxlat": 56.1759101, + "maxlon": 10.0665261 + }, + "geometry": [ + { "lat": 56.1753107, "lon": 10.0655336 }, + { "lat": 56.1753071, "lon": 10.0655382 }, + { "lat": 56.1752193, "lon": 10.0655474 }, + { "lat": 56.1752108, "lon": 10.0656623 }, + { "lat": 56.1752173, "lon": 10.0658327 }, + { "lat": 56.1753043, "lon": 10.0659677 }, + { "lat": 56.1753964, "lon": 10.0660981 }, + { "lat": 56.1754902, "lon": 10.0662151 }, + { "lat": 56.1756100, "lon": 10.0663485 }, + { "lat": 56.1757022, "lon": 10.0664373 }, + { "lat": 56.1757026, "lon": 10.0663036 }, + { "lat": 56.1757403, "lon": 10.0663405 }, + { "lat": 56.1757418, "lon": 10.0664726 }, + { "lat": 56.1757985, "lon": 10.0665261 }, + { "lat": 56.1757988, "lon": 10.0664709 }, + { "lat": 56.1757992, "lon": 10.0663988 }, + { "lat": 56.1757993, "lon": 10.0663847 }, + { "lat": 56.1758127, "lon": 10.0663251 }, + { "lat": 56.1757862, "lon": 10.0663009 }, + { "lat": 56.1757847, "lon": 10.0662939 }, + { "lat": 56.1757849, "lon": 10.0662863 }, + { "lat": 56.1757868, "lon": 10.0662801 }, + { "lat": 56.1758383, "lon": 10.0662558 }, + { "lat": 56.1758752, "lon": 10.0662384 }, + { "lat": 56.1759101, "lon": 10.0662219 }, + { "lat": 56.1758037, "lon": 10.0661467 }, + { "lat": 56.1756940, "lon": 10.0660428 }, + { "lat": 56.1755916, "lon": 10.0659242 }, + { "lat": 56.1755863, "lon": 10.0659351 }, + { "lat": 56.1755839, "lon": 10.0659385 }, + { "lat": 56.1755049, "lon": 10.0659742 }, + { "lat": 56.1754983, "lon": 10.0659754 }, + { "lat": 56.1754901, "lon": 10.0659734 }, + { "lat": 56.1754830, "lon": 10.0659678 }, + { "lat": 56.1753633, "lon": 10.0658030 }, + { "lat": 56.1753619, "lon": 10.0657983 }, + { "lat": 56.1753623, "lon": 10.0657935 }, + { "lat": 56.1753649, "lon": 10.0657906 }, + { "lat": 56.1754604, "lon": 10.0657789 }, + { "lat": 56.1754676, "lon": 10.0657678 }, + { "lat": 56.1753237, "lon": 10.0655325 }, + { "lat": 56.1753107, "lon": 10.0655336 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "36", + "capacity:charging": "2", + "capacity:disabled": "2", + "caravan": "designated", + "fee": "no", + "motorcar": "designated", + "motorcycle": "designated", + "opening_hours": "24/7", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 37930622, + "bounds": { + "minlat": 56.1767006, + "minlon": 10.1992429, + "maxlat": 56.1767467, + "maxlon": 10.1996547 + }, + "geometry": [ + { "lat": 56.1767467, "lon": 10.1992445 }, + { "lat": 56.1767036, "lon": 10.1992429 }, + { "lat": 56.1767006, "lon": 10.1996547 }, + { "lat": 56.1767428, "lon": 10.1996505 }, + { "lat": 56.1767467, "lon": 10.1992445 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity:disabled": "yes", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 38036160, + "bounds": { + "minlat": 56.0909791, + "minlon": 10.1365739, + "maxlat": 56.0916698, + "maxlon": 10.1386065 + }, + "geometry": [ + { "lat": 56.0916698, "lon": 10.1367667 }, + { "lat": 56.0913291, "lon": 10.1365739 }, + { "lat": 56.0909791, "lon": 10.1385512 }, + { "lat": 56.0910694, "lon": 10.1386065 }, + { "lat": 56.0912186, "lon": 10.1380357 }, + { "lat": 56.0914259, "lon": 10.1373877 }, + { "lat": 56.0916698, "lon": 10.1367667 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "yes", + "parking": "surface", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 38036161, + "bounds": { + "minlat": 56.0905065, + "minlon": 10.1369197, + "maxlat": 56.0911395, + "maxlon": 10.1386850 + }, + "geometry": [ + { "lat": 56.0911395, "lon": 10.1371540 }, + { "lat": 56.0907343, "lon": 10.1369197 }, + { "lat": 56.0905875, "lon": 10.1377354 }, + { "lat": 56.0905065, "lon": 10.1381857 }, + { "lat": 56.0905985, "lon": 10.1382389 }, + { "lat": 56.0905495, "lon": 10.1385068 }, + { "lat": 56.0908640, "lon": 10.1386850 }, + { "lat": 56.0911395, "lon": 10.1371540 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "yes", + "parking": "surface", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 53470679, + "bounds": { + "minlat": 56.1547442, + "minlon": 10.2056509, + "maxlat": 56.1551520, + "maxlon": 10.2061176 + }, + "geometry": [ + { "lat": 56.1551287, "lon": 10.2061176 }, + { "lat": 56.1551520, "lon": 10.2060460 }, + { "lat": 56.1550416, "lon": 10.2059325 }, + { "lat": 56.1547679, "lon": 10.2056509 }, + { "lat": 56.1547442, "lon": 10.2057246 }, + { "lat": 56.1547473, "lon": 10.2057278 }, + { "lat": 56.1547846, "lon": 10.2057656 }, + { "lat": 56.1550190, "lon": 10.2060054 }, + { "lat": 56.1551287, "lon": 10.2061176 } + ], + "tags": { + "amenity": "parking", + "capacity": "20", + "capacity:charging": "6", + "capacity:disabled": "2", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 55786220, + "bounds": { + "minlat": 56.1779454, + "minlon": 10.0654344, + "maxlat": 56.1787015, + "maxlon": 10.0662243 + }, + "geometry": [ + { "lat": 56.1785625, "lon": 10.0662243 }, + { "lat": 56.1784657, "lon": 10.0661205 }, + { "lat": 56.1783784, "lon": 10.0660320 }, + { "lat": 56.1783888, "lon": 10.0660146 }, + { "lat": 56.1784710, "lon": 10.0659784 }, + { "lat": 56.1784738, "lon": 10.0659744 }, + { "lat": 56.1784761, "lon": 10.0659672 }, + { "lat": 56.1784763, "lon": 10.0659591 }, + { "lat": 56.1784739, "lon": 10.0659536 }, + { "lat": 56.1783903, "lon": 10.0658792 }, + { "lat": 56.1783856, "lon": 10.0658773 }, + { "lat": 56.1783812, "lon": 10.0658776 }, + { "lat": 56.1783765, "lon": 10.0658805 }, + { "lat": 56.1782978, "lon": 10.0659335 }, + { "lat": 56.1782896, "lon": 10.0659569 }, + { "lat": 56.1781171, "lon": 10.0658356 }, + { "lat": 56.1779454, "lon": 10.0657293 }, + { "lat": 56.1779466, "lon": 10.0657190 }, + { "lat": 56.1780681, "lon": 10.0656383 }, + { "lat": 56.1780704, "lon": 10.0656326 }, + { "lat": 56.1780990, "lon": 10.0655616 }, + { "lat": 56.1780793, "lon": 10.0654344 }, + { "lat": 56.1782321, "lon": 10.0655372 }, + { "lat": 56.1783810, "lon": 10.0656525 }, + { "lat": 56.1785292, "lon": 10.0657859 }, + { "lat": 56.1786730, "lon": 10.0659342 }, + { "lat": 56.1787015, "lon": 10.0660997 }, + { "lat": 56.1786788, "lon": 10.0661682 }, + { "lat": 56.1786706, "lon": 10.0661635 }, + { "lat": 56.1786617, "lon": 10.0661628 }, + { "lat": 56.1786394, "lon": 10.0661722 }, + { "lat": 56.1785978, "lon": 10.0661899 }, + { "lat": 56.1785747, "lon": 10.0661997 }, + { "lat": 56.1785714, "lon": 10.0662030 }, + { "lat": 56.1785625, "lon": 10.0662243 } + ], + "tags": { + "amenity": "parking", + "capacity": "37", + "capacity:charging": "2", + "capacity:disabled": "2", + "caravan": "designated", + "fee": "no", + "motorcar": "designated", + "motorcycle": "designated", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 89048632, + "bounds": { + "minlat": 56.1673927, + "minlon": 10.2063130, + "maxlat": 56.1680742, + "maxlon": 10.2070312 + }, + "geometry": [ + { "lat": 56.1673927, "lon": 10.2065578 }, + { "lat": 56.1674765, "lon": 10.2065016 }, + { "lat": 56.1675070, "lon": 10.2066481 }, + { "lat": 56.1680062, "lon": 10.2063130 }, + { "lat": 56.1680742, "lon": 10.2066398 }, + { "lat": 56.1674912, "lon": 10.2070312 }, + { "lat": 56.1673927, "lon": 10.2065578 } + ], + "tags": { + "access": "no", + "access:conditional": "yes @ (Mo-Fr 00:00-07:00,17:00-24:00; Sa-Su 00:00-24:00)", + "amenity": "parking", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 107343720, + "bounds": { + "minlat": 56.1587072, + "minlon": 10.2042891, + "maxlat": 56.1587895, + "maxlon": 10.2052260 + }, + "geometry": [ + { "lat": 56.1587895, "lon": 10.2042891 }, + { "lat": 56.1587495, "lon": 10.2043449 }, + { "lat": 56.1587072, "lon": 10.2052260 }, + { "lat": 56.1587419, "lon": 10.2051743 }, + { "lat": 56.1587895, "lon": 10.2042891 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity:disabled": "3", + "fee": "yes", + "fee:conditional": "no @ (Mo-Sa 23:00-08:00; Su)", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 160617931, + "bounds": { + "minlat": 56.1694669, + "minlon": 10.1749166, + "maxlat": 56.1712802, + "maxlon": 10.1757387 + }, + "geometry": [ + { "lat": 56.1694955, "lon": 10.1757387 }, + { "lat": 56.1694669, "lon": 10.1752621 }, + { "lat": 56.1712516, "lon": 10.1749166 }, + { "lat": 56.1712766, "lon": 10.1753338 }, + { "lat": 56.1712802, "lon": 10.1753932 }, + { "lat": 56.1708321, "lon": 10.1754799 }, + { "lat": 56.1694955, "lon": 10.1757387 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 164055492, + "bounds": { + "minlat": 56.1924543, + "minlon": 10.1829704, + "maxlat": 56.1940349, + "maxlon": 10.1857833 + }, + "geometry": [ + { "lat": 56.1939703, "lon": 10.1829704 }, + { "lat": 56.1935791, "lon": 10.1832593 }, + { "lat": 56.1935978, "lon": 10.1833371 }, + { "lat": 56.1934045, "lon": 10.1834860 }, + { "lat": 56.1933877, "lon": 10.1834142 }, + { "lat": 56.1929877, "lon": 10.1837137 }, + { "lat": 56.1930035, "lon": 10.1837965 }, + { "lat": 56.1928080, "lon": 10.1839386 }, + { "lat": 56.1927871, "lon": 10.1838635 }, + { "lat": 56.1924543, "lon": 10.1841290 }, + { "lat": 56.1925005, "lon": 10.1843235 }, + { "lat": 56.1928926, "lon": 10.1840070 }, + { "lat": 56.1928999, "lon": 10.1840379 }, + { "lat": 56.1928577, "lon": 10.1840699 }, + { "lat": 56.1929453, "lon": 10.1844420 }, + { "lat": 56.1925863, "lon": 10.1847151 }, + { "lat": 56.1928471, "lon": 10.1857833 }, + { "lat": 56.1929800, "lon": 10.1856827 }, + { "lat": 56.1927460, "lon": 10.1846876 }, + { "lat": 56.1929640, "lon": 10.1845211 }, + { "lat": 56.1931329, "lon": 10.1852399 }, + { "lat": 56.1931447, "lon": 10.1852880 }, + { "lat": 56.1931559, "lon": 10.1853358 }, + { "lat": 56.1931976, "lon": 10.1853008 }, + { "lat": 56.1932161, "lon": 10.1853791 }, + { "lat": 56.1937034, "lon": 10.1850034 }, + { "lat": 56.1936851, "lon": 10.1849283 }, + { "lat": 56.1939074, "lon": 10.1847605 }, + { "lat": 56.1938967, "lon": 10.1847141 }, + { "lat": 56.1938875, "lon": 10.1846739 }, + { "lat": 56.1936160, "lon": 10.1834939 }, + { "lat": 56.1935681, "lon": 10.1835306 }, + { "lat": 56.1935695, "lon": 10.1834890 }, + { "lat": 56.1939756, "lon": 10.1832078 }, + { "lat": 56.1939904, "lon": 10.1832730 }, + { "lat": 56.1940349, "lon": 10.1832351 }, + { "lat": 56.1939703, "lon": 10.1829704 } + ], + "tags": { + "access": "private", + "amenity": "parking", + "capacity:disabled": "2", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 165863146, + "bounds": { + "minlat": 56.1974693, + "minlon": 10.1773423, + "maxlat": 56.1979674, + "maxlon": 10.1787317 + }, + "geometry": [ + { "lat": 56.1976083, "lon": 10.1773423 }, + { "lat": 56.1975384, "lon": 10.1774102 }, + { "lat": 56.1974693, "lon": 10.1774773 }, + { "lat": 56.1978285, "lon": 10.1786714 }, + { "lat": 56.1978693, "lon": 10.1786316 }, + { "lat": 56.1978782, "lon": 10.1786610 }, + { "lat": 56.1978994, "lon": 10.1787317 }, + { "lat": 56.1979572, "lon": 10.1786756 }, + { "lat": 56.1979363, "lon": 10.1786060 }, + { "lat": 56.1979270, "lon": 10.1785753 }, + { "lat": 56.1979674, "lon": 10.1785361 }, + { "lat": 56.1976083, "lon": 10.1773423 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "62", + "capacity:disabled": "2", + "fee": "no", + "lit": "yes", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 165863151, + "bounds": { + "minlat": 56.1972996, + "minlon": 10.1774656, + "maxlat": 56.1978062, + "maxlon": 10.1788888 + }, + "geometry": [ + { "lat": 56.1974397, "lon": 10.1774656 }, + { "lat": 56.1973846, "lon": 10.1775189 }, + { "lat": 56.1973809, "lon": 10.1775224 }, + { "lat": 56.1973762, "lon": 10.1775270 }, + { "lat": 56.1973709, "lon": 10.1775321 }, + { "lat": 56.1972996, "lon": 10.1776010 }, + { "lat": 56.1975028, "lon": 10.1782807 }, + { "lat": 56.1975755, "lon": 10.1785238 }, + { "lat": 56.1976507, "lon": 10.1787755 }, + { "lat": 56.1976658, "lon": 10.1788259 }, + { "lat": 56.1977084, "lon": 10.1787848 }, + { "lat": 56.1977179, "lon": 10.1788168 }, + { "lat": 56.1977394, "lon": 10.1788888 }, + { "lat": 56.1977973, "lon": 10.1788328 }, + { "lat": 56.1977665, "lon": 10.1787298 }, + { "lat": 56.1978062, "lon": 10.1786915 }, + { "lat": 56.1974397, "lon": 10.1774656 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "66", + "capacity:disabled": "2", + "fee": "no", + "lit": "yes", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 165863156, + "bounds": { + "minlat": 56.1976532, + "minlon": 10.1772629, + "maxlat": 56.1981261, + "maxlon": 10.1785803 + }, + "geometry": [ + { "lat": 56.1977918, "lon": 10.1772629 }, + { "lat": 56.1977216, "lon": 10.1773308 }, + { "lat": 56.1976532, "lon": 10.1773970 }, + { "lat": 56.1979886, "lon": 10.1785171 }, + { "lat": 56.1980282, "lon": 10.1784788 }, + { "lat": 56.1980371, "lon": 10.1785086 }, + { "lat": 56.1980586, "lon": 10.1785803 }, + { "lat": 56.1981171, "lon": 10.1785237 }, + { "lat": 56.1980959, "lon": 10.1784530 }, + { "lat": 56.1980856, "lon": 10.1784185 }, + { "lat": 56.1981261, "lon": 10.1783794 }, + { "lat": 56.1977918, "lon": 10.1772629 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "58", + "capacity:disabled": "2", + "fee": "no", + "lit": "yes", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 165863158, + "bounds": { + "minlat": 56.1971168, + "minlon": 10.1775858, + "maxlat": 56.1976453, + "maxlon": 10.1790435 + }, + "geometry": [ + { "lat": 56.1972665, "lon": 10.1775858 }, + { "lat": 56.1971589, "lon": 10.1776496 }, + { "lat": 56.1971168, "lon": 10.1776744 }, + { "lat": 56.1975056, "lon": 10.1789629 }, + { "lat": 56.1975448, "lon": 10.1789268 }, + { "lat": 56.1975795, "lon": 10.1790435 }, + { "lat": 56.1976366, "lon": 10.1789870 }, + { "lat": 56.1976142, "lon": 10.1789168 }, + { "lat": 56.1976047, "lon": 10.1788849 }, + { "lat": 56.1976453, "lon": 10.1788457 }, + { "lat": 56.1975236, "lon": 10.1784383 }, + { "lat": 56.1974584, "lon": 10.1785012 }, + { "lat": 56.1974494, "lon": 10.1784712 }, + { "lat": 56.1975127, "lon": 10.1784101 }, + { "lat": 56.1972665, "lon": 10.1775858 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "33", + "capacity:disabled": "1", + "fee": "no", + "lit": "yes", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 174488858, + "bounds": { + "minlat": 56.1697704, + "minlon": 10.1845796, + "maxlat": 56.1702644, + "maxlon": 10.1849799 + }, + "geometry": [ + { "lat": 56.1697931, "lon": 10.1845870 }, + { "lat": 56.1697779, "lon": 10.1847639 }, + { "lat": 56.1697704, "lon": 10.1848503 }, + { "lat": 56.1699250, "lon": 10.1848932 }, + { "lat": 56.1699652, "lon": 10.1849044 }, + { "lat": 56.1699989, "lon": 10.1849137 }, + { "lat": 56.1702376, "lon": 10.1849799 }, + { "lat": 56.1702644, "lon": 10.1846679 }, + { "lat": 56.1699626, "lon": 10.1845796 }, + { "lat": 56.1699585, "lon": 10.1846329 }, + { "lat": 56.1697931, "lon": 10.1845870 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 177971436, + "bounds": { + "minlat": 56.1721931, + "minlon": 10.1886438, + "maxlat": 56.1722310, + "maxlon": 10.1887616 + }, + "geometry": [ + { "lat": 56.1722078, "lon": 10.1886438 }, + { "lat": 56.1722310, "lon": 10.1886522 }, + { "lat": 56.1722166, "lon": 10.1887616 }, + { "lat": 56.1721931, "lon": 10.1887518 }, + { "lat": 56.1722078, "lon": 10.1886438 } + ], + "tags": { + "access": "destination", + "amenity": "parking", + "capacity:disabled": "1", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 224416078, + "bounds": { + "minlat": 56.2712622, + "minlon": 10.3050297, + "maxlat": 56.2718102, + "maxlon": 10.3060124 + }, + "geometry": [ + { "lat": 56.2713974, "lon": 10.3050297 }, + { "lat": 56.2712622, "lon": 10.3057425 }, + { "lat": 56.2716810, "lon": 10.3060124 }, + { "lat": 56.2717386, "lon": 10.3058837 }, + { "lat": 56.2718070, "lon": 10.3055363 }, + { "lat": 56.2718102, "lon": 10.3053096 }, + { "lat": 56.2713974, "lon": 10.3050297 } + ], + "tags": { + "amenity": "parking", + "capacity": "76", + "capacity:disabled": "4", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 231427422, + "bounds": { + "minlat": 56.1518987, + "minlon": 10.2030347, + "maxlat": 56.1523646, + "maxlon": 10.2034265 + }, + "geometry": [ + { "lat": 56.1519150, "lon": 10.2030928 }, + { "lat": 56.1518987, "lon": 10.2032136 }, + { "lat": 56.1523344, "lon": 10.2034265 }, + { "lat": 56.1523403, "lon": 10.2034110 }, + { "lat": 56.1523532, "lon": 10.2033274 }, + { "lat": 56.1523646, "lon": 10.2032532 }, + { "lat": 56.1519240, "lon": 10.2030347 }, + { "lat": 56.1519150, "lon": 10.2030928 } + ], + "tags": { + "access": "private", + "amenity": "parking", + "capacity": "17", + "capacity:disabled": "1", + "orientation": "diagonal", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 268546605, + "bounds": { + "minlat": 56.1503165, + "minlon": 10.2036038, + "maxlat": 56.1504432, + "maxlon": 10.2038177 + }, + "geometry": [ + { "lat": 56.1504432, "lon": 10.2038177 }, + { "lat": 56.1504358, "lon": 10.2037697 }, + { "lat": 56.1504298, "lon": 10.2037311 }, + { "lat": 56.1504169, "lon": 10.2036478 }, + { "lat": 56.1503659, "lon": 10.2036038 }, + { "lat": 56.1503165, "lon": 10.2036226 }, + { "lat": 56.1503423, "lon": 10.2037935 }, + { "lat": 56.1503912, "lon": 10.2037702 }, + { "lat": 56.1504432, "lon": 10.2038177 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface", + "source": "Geodatastyrelsen : FOT ortofoto" + } +}, +{ + "type": "way", + "id": 268546648, + "bounds": { + "minlat": 56.1511383, + "minlon": 10.2027409, + "maxlat": 56.1512144, + "maxlon": 10.2028174 + }, + "geometry": [ + { "lat": 56.1512089, "lon": 10.2027409 }, + { "lat": 56.1511383, "lon": 10.2027739 }, + { "lat": 56.1511435, "lon": 10.2028174 }, + { "lat": 56.1512144, "lon": 10.2027828 }, + { "lat": 56.1512089, "lon": 10.2027409 } + ], + "tags": { + "access": "yes", + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled", + "source": "Geodatastyrelsen : FOT ortofoto" + } +}, +{ + "type": "way", + "id": 268546649, + "bounds": { + "minlat": 56.1510953, + "minlon": 10.2020203, + "maxlat": 56.1512144, + "maxlon": 10.2028174 + }, + "geometry": [ + { "lat": 56.1512089, "lon": 10.2027409 }, + { "lat": 56.1512037, "lon": 10.2027002 }, + { "lat": 56.1511904, "lon": 10.2025940 }, + { "lat": 56.1511802, "lon": 10.2023111 }, + { "lat": 56.1511709, "lon": 10.2020595 }, + { "lat": 56.1510953, "lon": 10.2020203 }, + { "lat": 56.1511094, "lon": 10.2025229 }, + { "lat": 56.1511341, "lon": 10.2027362 }, + { "lat": 56.1511383, "lon": 10.2027739 }, + { "lat": 56.1511435, "lon": 10.2028174 }, + { "lat": 56.1512144, "lon": 10.2027828 }, + { "lat": 56.1512089, "lon": 10.2027409 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "19", + "capacity:disabled": "2", + "fee": "yes", + "parking": "surface", + "source": "Geodatastyrelsen : FOT ortofoto" + } +}, +{ + "type": "way", + "id": 294333330, + "bounds": { + "minlat": 56.1893800, + "minlon": 10.1753553, + "maxlat": 56.1905511, + "maxlon": 10.1766990 + }, + "geometry": [ + { "lat": 56.1903515, "lon": 10.1757836 }, + { "lat": 56.1904377, "lon": 10.1757281 }, + { "lat": 56.1904742, "lon": 10.1757025 }, + { "lat": 56.1905117, "lon": 10.1756761 }, + { "lat": 56.1905511, "lon": 10.1756484 }, + { "lat": 56.1905254, "lon": 10.1755302 }, + { "lat": 56.1904851, "lon": 10.1753553 }, + { "lat": 56.1899937, "lon": 10.1757071 }, + { "lat": 56.1900220, "lon": 10.1758175 }, + { "lat": 56.1900220, "lon": 10.1758866 }, + { "lat": 56.1900408, "lon": 10.1760007 }, + { "lat": 56.1893800, "lon": 10.1764595 }, + { "lat": 56.1894169, "lon": 10.1766391 }, + { "lat": 56.1894403, "lon": 10.1766246 }, + { "lat": 56.1894538, "lon": 10.1766990 }, + { "lat": 56.1904049, "lon": 10.1760279 }, + { "lat": 56.1903515, "lon": 10.1757836 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "119", + "capacity:disabled": "3", + "fee": "yes", + "name": "P8", + "parking": "surface", + "source": "survey" + } +}, +{ + "type": "way", + "id": 302899494, + "bounds": { + "minlat": 56.1733336, + "minlon": 10.2153042, + "maxlat": 56.1733830, + "maxlon": 10.2153415 + }, + "geometry": [ + { "lat": 56.1733336, "lon": 10.2153371 }, + { "lat": 56.1733345, "lon": 10.2153042 }, + { "lat": 56.1733830, "lon": 10.2153086 }, + { "lat": 56.1733821, "lon": 10.2153415 }, + { "lat": 56.1733336, "lon": 10.2153371 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 302902893, + "bounds": { + "minlat": 56.1452078, + "minlon": 10.1889690, + "maxlat": 56.1452924, + "maxlon": 10.1891162 + }, + "geometry": [ + { "lat": 56.1452078, "lon": 10.1890932 }, + { "lat": 56.1452801, "lon": 10.1889690 }, + { "lat": 56.1452924, "lon": 10.1889920 }, + { "lat": 56.1452200, "lon": 10.1891162 }, + { "lat": 56.1452078, "lon": 10.1890932 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 302903845, + "bounds": { + "minlat": 56.1477681, + "minlon": 10.1920248, + "maxlat": 56.1478074, + "maxlon": 10.1920675 + }, + "geometry": [ + { "lat": 56.1478036, "lon": 10.1920248 }, + { "lat": 56.1478074, "lon": 10.1920502 }, + { "lat": 56.1477719, "lon": 10.1920675 }, + { "lat": 56.1477681, "lon": 10.1920421 }, + { "lat": 56.1478036, "lon": 10.1920248 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 302903907, + "bounds": { + "minlat": 56.1482591, + "minlon": 10.1933036, + "maxlat": 56.1483063, + "maxlon": 10.1933489 + }, + "geometry": [ + { "lat": 56.1483027, "lon": 10.1933036 }, + { "lat": 56.1483063, "lon": 10.1933292 }, + { "lat": 56.1482626, "lon": 10.1933489 }, + { "lat": 56.1482591, "lon": 10.1933234 }, + { "lat": 56.1483027, "lon": 10.1933036 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 302903908, + "bounds": { + "minlat": 56.1481800, + "minlon": 10.1933401, + "maxlat": 56.1482289, + "maxlon": 10.1933837 + }, + "geometry": [ + { "lat": 56.1481831, "lon": 10.1933837 }, + { "lat": 56.1481800, "lon": 10.1933589 }, + { "lat": 56.1482257, "lon": 10.1933401 }, + { "lat": 56.1482289, "lon": 10.1933649 }, + { "lat": 56.1481831, "lon": 10.1933837 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 302904107, + "bounds": { + "minlat": 56.1476183, + "minlon": 10.1942725, + "maxlat": 56.1476632, + "maxlon": 10.1943434 + }, + "geometry": [ + { "lat": 56.1476288, "lon": 10.1942725 }, + { "lat": 56.1476183, "lon": 10.1942978 }, + { "lat": 56.1476527, "lon": 10.1943434 }, + { "lat": 56.1476632, "lon": 10.1943181 }, + { "lat": 56.1476288, "lon": 10.1942725 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 302904624, + "bounds": { + "minlat": 56.1478805, + "minlon": 10.1974457, + "maxlat": 56.1479383, + "maxlon": 10.1974853 + }, + "geometry": [ + { "lat": 56.1478805, "lon": 10.1974839 }, + { "lat": 56.1478808, "lon": 10.1974457 }, + { "lat": 56.1479383, "lon": 10.1974471 }, + { "lat": 56.1479380, "lon": 10.1974853 }, + { "lat": 56.1478805, "lon": 10.1974839 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side", + "wheelchair": "designated" + } +}, +{ + "type": "way", + "id": 372076169, + "bounds": { + "minlat": 56.1760878, + "minlon": 10.1367126, + "maxlat": 56.1774428, + "maxlon": 10.1376591 + }, + "geometry": [ + { "lat": 56.1774428, "lon": 10.1368157 }, + { "lat": 56.1769568, "lon": 10.1367688 }, + { "lat": 56.1769247, "lon": 10.1369168 }, + { "lat": 56.1768022, "lon": 10.1368229 }, + { "lat": 56.1766961, "lon": 10.1367345 }, + { "lat": 56.1764247, "lon": 10.1367126 }, + { "lat": 56.1761866, "lon": 10.1367575 }, + { "lat": 56.1761634, "lon": 10.1368499 }, + { "lat": 56.1761234, "lon": 10.1370089 }, + { "lat": 56.1761647, "lon": 10.1370435 }, + { "lat": 56.1760919, "lon": 10.1373312 }, + { "lat": 56.1760878, "lon": 10.1373815 }, + { "lat": 56.1760982, "lon": 10.1374237 }, + { "lat": 56.1761205, "lon": 10.1374541 }, + { "lat": 56.1763965, "lon": 10.1376571 }, + { "lat": 56.1764080, "lon": 10.1376591 }, + { "lat": 56.1764200, "lon": 10.1376484 }, + { "lat": 56.1764288, "lon": 10.1376270 }, + { "lat": 56.1764816, "lon": 10.1373902 }, + { "lat": 56.1764958, "lon": 10.1373607 }, + { "lat": 56.1765126, "lon": 10.1373496 }, + { "lat": 56.1765349, "lon": 10.1373526 }, + { "lat": 56.1766384, "lon": 10.1374335 }, + { "lat": 56.1766646, "lon": 10.1373201 }, + { "lat": 56.1767050, "lon": 10.1373502 }, + { "lat": 56.1767228, "lon": 10.1372731 }, + { "lat": 56.1766827, "lon": 10.1372432 }, + { "lat": 56.1766849, "lon": 10.1372338 }, + { "lat": 56.1767508, "lon": 10.1369531 }, + { "lat": 56.1767559, "lon": 10.1369386 }, + { "lat": 56.1767669, "lon": 10.1369295 }, + { "lat": 56.1767783, "lon": 10.1369306 }, + { "lat": 56.1769143, "lon": 10.1370306 }, + { "lat": 56.1768972, "lon": 10.1371038 }, + { "lat": 56.1769358, "lon": 10.1371329 }, + { "lat": 56.1772643, "lon": 10.1373783 }, + { "lat": 56.1772902, "lon": 10.1372682 }, + { "lat": 56.1773326, "lon": 10.1372991 }, + { "lat": 56.1774428, "lon": 10.1368157 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity:charging": "2", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 380273552, + "bounds": { + "minlat": 56.1273653, + "minlon": 10.1767284, + "maxlat": 56.1274815, + "maxlon": 10.1769265 + }, + "geometry": [ + { "lat": 56.1274716, "lon": 10.1767284 }, + { "lat": 56.1274815, "lon": 10.1769017 }, + { "lat": 56.1273739, "lon": 10.1769265 }, + { "lat": 56.1273653, "lon": 10.1767614 }, + { "lat": 56.1274716, "lon": 10.1767284 } + ], + "tags": { + "access": "customers", + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "2", + "fee": "no", + "parking": "street_side", + "supervised": "no" + } +}, +{ + "type": "way", + "id": 384028175, + "bounds": { + "minlat": 56.1572328, + "minlon": 10.2101549, + "maxlat": 56.1572932, + "maxlon": 10.2102509 + }, + "geometry": [ + { "lat": 56.1572442, "lon": 10.2101549 }, + { "lat": 56.1572328, "lon": 10.2102254 }, + { "lat": 56.1572819, "lon": 10.2102509 }, + { "lat": 56.1572932, "lon": 10.2101805 }, + { "lat": 56.1572442, "lon": 10.2101549 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 384028178, + "bounds": { + "minlat": 56.1571704, + "minlon": 10.2101549, + "maxlat": 56.1572932, + "maxlon": 10.2106368 + }, + "geometry": [ + { "lat": 56.1572819, "lon": 10.2102509 }, + { "lat": 56.1572692, "lon": 10.2103294 }, + { "lat": 56.1572195, "lon": 10.2106368 }, + { "lat": 56.1571704, "lon": 10.2106112 }, + { "lat": 56.1572201, "lon": 10.2103042 }, + { "lat": 56.1572328, "lon": 10.2102254 }, + { "lat": 56.1572442, "lon": 10.2101549 }, + { "lat": 56.1572932, "lon": 10.2101805 }, + { "lat": 56.1572819, "lon": 10.2102509 } + ], + "tags": { + "amenity": "parking", + "capacity:charging": "8", + "capacity:disabled": "1", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 384028179, + "bounds": { + "minlat": 56.1522109, + "minlon": 10.2033142, + "maxlat": 56.1522732, + "maxlon": 10.2033746 + }, + "geometry": [ + { "lat": 56.1522109, "lon": 10.2033621 }, + { "lat": 56.1522493, "lon": 10.2033142 }, + { "lat": 56.1522732, "lon": 10.2033249 }, + { "lat": 56.1522334, "lon": 10.2033746 }, + { "lat": 56.1522109, "lon": 10.2033621 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 384028180, + "bounds": { + "minlat": 56.1575606, + "minlon": 10.2100571, + "maxlat": 56.1576625, + "maxlon": 10.2101865 + }, + "geometry": [ + { "lat": 56.1576625, "lon": 10.2101038 }, + { "lat": 56.1576490, "lon": 10.2101865 }, + { "lat": 56.1575606, "lon": 10.2101397 }, + { "lat": 56.1575741, "lon": 10.2100571 }, + { "lat": 56.1576625, "lon": 10.2101038 } + ], + "tags": { + "amenity": "parking", + "capacity": "3", + "capacity:disabled": "3", + "orientation": "perpendicular", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 401589969, + "bounds": { + "minlat": 56.0903281, + "minlon": 10.1383689, + "maxlat": 56.0908415, + "maxlon": 10.1390814 + }, + "geometry": [ + { "lat": 56.0908415, "lon": 10.1388082 }, + { "lat": 56.0907928, "lon": 10.1390814 }, + { "lat": 56.0903281, "lon": 10.1388155 }, + { "lat": 56.0904076, "lon": 10.1383689 }, + { "lat": 56.0904605, "lon": 10.1383992 }, + { "lat": 56.0904296, "lon": 10.1385726 }, + { "lat": 56.0908415, "lon": 10.1388082 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "yes", + "parking": "surface", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 432183115, + "bounds": { + "minlat": 56.0402987, + "minlon": 10.2703495, + "maxlat": 56.0403253, + "maxlon": 10.2704337 + }, + "geometry": [ + { "lat": 56.0403225, "lon": 10.2703495 }, + { "lat": 56.0403253, "lon": 10.2704311 }, + { "lat": 56.0403014, "lon": 10.2704337 }, + { "lat": 56.0402987, "lon": 10.2703521 }, + { "lat": 56.0403225, "lon": 10.2703495 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity:disabled": "yes", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 460195735, + "bounds": { + "minlat": 56.1967367, + "minlon": 10.1796637, + "maxlat": 56.1967858, + "maxlon": 10.1797243 + }, + "geometry": [ + { "lat": 56.1967858, "lon": 10.1796684 }, + { "lat": 56.1967385, "lon": 10.1796637 }, + { "lat": 56.1967367, "lon": 10.1797195 }, + { "lat": 56.1967840, "lon": 10.1797243 }, + { "lat": 56.1967858, "lon": 10.1796684 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 460195736, + "bounds": { + "minlat": 56.1967385, + "minlon": 10.1796078, + "maxlat": 56.1967876, + "maxlon": 10.1796684 + }, + "geometry": [ + { "lat": 56.1967876, "lon": 10.1796127 }, + { "lat": 56.1967403, "lon": 10.1796078 }, + { "lat": 56.1967385, "lon": 10.1796637 }, + { "lat": 56.1967858, "lon": 10.1796684 }, + { "lat": 56.1967876, "lon": 10.1796127 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 460195737, + "bounds": { + "minlat": 56.1967403, + "minlon": 10.1795519, + "maxlat": 56.1967893, + "maxlon": 10.1796127 + }, + "geometry": [ + { "lat": 56.1967893, "lon": 10.1795568 }, + { "lat": 56.1967420, "lon": 10.1795519 }, + { "lat": 56.1967403, "lon": 10.1796078 }, + { "lat": 56.1967876, "lon": 10.1796127 }, + { "lat": 56.1967893, "lon": 10.1795568 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 460195738, + "bounds": { + "minlat": 56.1968501, + "minlon": 10.1796195, + "maxlat": 56.1968992, + "maxlon": 10.1796802 + }, + "geometry": [ + { "lat": 56.1968992, "lon": 10.1796243 }, + { "lat": 56.1968519, "lon": 10.1796195 }, + { "lat": 56.1968501, "lon": 10.1796754 }, + { "lat": 56.1968974, "lon": 10.1796802 }, + { "lat": 56.1968992, "lon": 10.1796243 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 460195739, + "bounds": { + "minlat": 56.1968519, + "minlon": 10.1795636, + "maxlat": 56.1969010, + "maxlon": 10.1796243 + }, + "geometry": [ + { "lat": 56.1969010, "lon": 10.1795684 }, + { "lat": 56.1968537, "lon": 10.1795636 }, + { "lat": 56.1968519, "lon": 10.1796195 }, + { "lat": 56.1968992, "lon": 10.1796243 }, + { "lat": 56.1969010, "lon": 10.1795684 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 460200521, + "bounds": { + "minlat": 56.1986022, + "minlon": 10.1793457, + "maxlat": 56.1991353, + "maxlon": 10.1800216 + }, + "geometry": [ + { "lat": 56.1986022, "lon": 10.1798788 }, + { "lat": 56.1986197, "lon": 10.1798619 }, + { "lat": 56.1986369, "lon": 10.1798453 }, + { "lat": 56.1986153, "lon": 10.1797755 }, + { "lat": 56.1990656, "lon": 10.1793457 }, + { "lat": 56.1991001, "lon": 10.1794698 }, + { "lat": 56.1991353, "lon": 10.1795907 }, + { "lat": 56.1991300, "lon": 10.1795958 }, + { "lat": 56.1991240, "lon": 10.1796016 }, + { "lat": 56.1986906, "lon": 10.1800216 }, + { "lat": 56.1986705, "lon": 10.1799506 }, + { "lat": 56.1986532, "lon": 10.1799668 }, + { "lat": 56.1986347, "lon": 10.1799841 }, + { "lat": 56.1986022, "lon": 10.1798788 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "42", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 460200522, + "bounds": { + "minlat": 56.1985189, + "minlon": 10.1790812, + "maxlat": 56.1990336, + "maxlon": 10.1797362 + }, + "geometry": [ + { "lat": 56.1985189, "lon": 10.1795975 }, + { "lat": 56.1985369, "lon": 10.1795803 }, + { "lat": 56.1985535, "lon": 10.1795643 }, + { "lat": 56.1985323, "lon": 10.1794930 }, + { "lat": 56.1989613, "lon": 10.1790812 }, + { "lat": 56.1989991, "lon": 10.1792086 }, + { "lat": 56.1990336, "lon": 10.1793246 }, + { "lat": 56.1986046, "lon": 10.1797362 }, + { "lat": 56.1985842, "lon": 10.1796675 }, + { "lat": 56.1985668, "lon": 10.1796842 }, + { "lat": 56.1985495, "lon": 10.1797008 }, + { "lat": 56.1985189, "lon": 10.1795975 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "40", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 460200523, + "bounds": { + "minlat": 56.1984347, + "minlon": 10.1788139, + "maxlat": 56.1989267, + "maxlon": 10.1794550 + }, + "geometry": [ + { "lat": 56.1988544, "lon": 10.1788139 }, + { "lat": 56.1988905, "lon": 10.1789387 }, + { "lat": 56.1989267, "lon": 10.1790601 }, + { "lat": 56.1985194, "lon": 10.1794550 }, + { "lat": 56.1984989, "lon": 10.1793873 }, + { "lat": 56.1984822, "lon": 10.1794015 }, + { "lat": 56.1984646, "lon": 10.1794188 }, + { "lat": 56.1984347, "lon": 10.1793163 }, + { "lat": 56.1984516, "lon": 10.1792999 }, + { "lat": 56.1984687, "lon": 10.1792840 }, + { "lat": 56.1984467, "lon": 10.1792109 }, + { "lat": 56.1988544, "lon": 10.1788139 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "38", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 460200524, + "bounds": { + "minlat": 56.1983489, + "minlon": 10.1785331, + "maxlat": 56.1988441, + "maxlon": 10.1791680 + }, + "geometry": [ + { "lat": 56.1987729, "lon": 10.1785331 }, + { "lat": 56.1988091, "lon": 10.1786578 }, + { "lat": 56.1988441, "lon": 10.1787789 }, + { "lat": 56.1984355, "lon": 10.1791680 }, + { "lat": 56.1984146, "lon": 10.1791003 }, + { "lat": 56.1983963, "lon": 10.1791157 }, + { "lat": 56.1983794, "lon": 10.1791321 }, + { "lat": 56.1983489, "lon": 10.1790313 }, + { "lat": 56.1983662, "lon": 10.1790141 }, + { "lat": 56.1983847, "lon": 10.1789957 }, + { "lat": 56.1983635, "lon": 10.1789266 }, + { "lat": 56.1987729, "lon": 10.1785331 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "38", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 460200525, + "bounds": { + "minlat": 56.1982629, + "minlon": 10.1782450, + "maxlat": 56.1987597, + "maxlon": 10.1788830 + }, + "geometry": [ + { "lat": 56.1982629, "lon": 10.1787468 }, + { "lat": 56.1982831, "lon": 10.1787274 }, + { "lat": 56.1982989, "lon": 10.1787120 }, + { "lat": 56.1982780, "lon": 10.1786443 }, + { "lat": 56.1986873, "lon": 10.1782450 }, + { "lat": 56.1987233, "lon": 10.1783700 }, + { "lat": 56.1987597, "lon": 10.1784913 }, + { "lat": 56.1983515, "lon": 10.1788830 }, + { "lat": 56.1983314, "lon": 10.1788126 }, + { "lat": 56.1983105, "lon": 10.1788324 }, + { "lat": 56.1982932, "lon": 10.1788496 }, + { "lat": 56.1982629, "lon": 10.1787468 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "38", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 460200526, + "bounds": { + "minlat": 56.1981788, + "minlon": 10.1779580, + "maxlat": 56.1986759, + "maxlon": 10.1785981 + }, + "geometry": [ + { "lat": 56.1986031, "lon": 10.1779580 }, + { "lat": 56.1986391, "lon": 10.1780830 }, + { "lat": 56.1986759, "lon": 10.1782059 }, + { "lat": 56.1982665, "lon": 10.1785981 }, + { "lat": 56.1982463, "lon": 10.1785357 }, + { "lat": 56.1982302, "lon": 10.1785495 }, + { "lat": 56.1982099, "lon": 10.1785690 }, + { "lat": 56.1981788, "lon": 10.1784626 }, + { "lat": 56.1982108, "lon": 10.1784344 }, + { "lat": 56.1982332, "lon": 10.1784134 }, + { "lat": 56.1982108, "lon": 10.1783349 }, + { "lat": 56.1986031, "lon": 10.1779580 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "37", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 460658676, + "bounds": { + "minlat": 56.2710821, + "minlon": 10.3075550, + "maxlat": 56.2714513, + "maxlon": 10.3082749 + }, + "geometry": [ + { "lat": 56.2711869, "lon": 10.3082749 }, + { "lat": 56.2712493, "lon": 10.3081265 }, + { "lat": 56.2712711, "lon": 10.3081562 }, + { "lat": 56.2712852, "lon": 10.3081224 }, + { "lat": 56.2714513, "lon": 10.3077276 }, + { "lat": 56.2713247, "lon": 10.3075550 }, + { "lat": 56.2710821, "lon": 10.3081321 }, + { "lat": 56.2711869, "lon": 10.3082749 } + ], + "tags": { + "amenity": "parking", + "capacity": "25", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 473904763, + "bounds": { + "minlat": 56.1908870, + "minlon": 10.2099595, + "maxlat": 56.1909301, + "maxlon": 10.2100524 + }, + "geometry": [ + { "lat": 56.1909301, "lon": 10.2099776 }, + { "lat": 56.1909152, "lon": 10.2100524 }, + { "lat": 56.1908870, "lon": 10.2100342 }, + { "lat": 56.1909019, "lon": 10.2099595 }, + { "lat": 56.1909301, "lon": 10.2099776 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 475380006, + "bounds": { + "minlat": 56.1530985, + "minlon": 10.1786061, + "maxlat": 56.1538256, + "maxlon": 10.1798635 + }, + "geometry": [ + { "lat": 56.1530985, "lon": 10.1789069 }, + { "lat": 56.1533285, "lon": 10.1795939 }, + { "lat": 56.1533449, "lon": 10.1796417 }, + { "lat": 56.1534187, "lon": 10.1798635 }, + { "lat": 56.1538256, "lon": 10.1792591 }, + { "lat": 56.1535159, "lon": 10.1786061 }, + { "lat": 56.1532594, "lon": 10.1790258 }, + { "lat": 56.1531757, "lon": 10.1788300 }, + { "lat": 56.1530985, "lon": 10.1789069 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "1", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 524385811, + "bounds": { + "minlat": 56.1418750, + "minlon": 10.2311435, + "maxlat": 56.1425277, + "maxlon": 10.2327856 + }, + "geometry": [ + { "lat": 56.1419763, "lon": 10.2316598 }, + { "lat": 56.1419451, "lon": 10.2316110 }, + { "lat": 56.1418750, "lon": 10.2314485 }, + { "lat": 56.1418828, "lon": 10.2313761 }, + { "lat": 56.1419124, "lon": 10.2313023 }, + { "lat": 56.1419720, "lon": 10.2312204 }, + { "lat": 56.1420459, "lon": 10.2311541 }, + { "lat": 56.1420599, "lon": 10.2311435 }, + { "lat": 56.1421046, "lon": 10.2311758 }, + { "lat": 56.1421444, "lon": 10.2313430 }, + { "lat": 56.1425218, "lon": 10.2322542 }, + { "lat": 56.1425277, "lon": 10.2323202 }, + { "lat": 56.1425128, "lon": 10.2323766 }, + { "lat": 56.1424526, "lon": 10.2324651 }, + { "lat": 56.1425267, "lon": 10.2326406 }, + { "lat": 56.1424253, "lon": 10.2327856 }, + { "lat": 56.1423562, "lon": 10.2326153 }, + { "lat": 56.1422819, "lon": 10.2323947 }, + { "lat": 56.1419763, "lon": 10.2316598 } + ], + "tags": { + "amenity": "parking", + "capacity:charging": "6", + "capacity:disabled": "2" + } +}, +{ + "type": "way", + "id": 652484745, + "bounds": { + "minlat": 56.1986001, + "minlon": 10.1572212, + "maxlat": 56.1991913, + "maxlon": 10.1583330 + }, + "geometry": [ + { "lat": 56.1987065, "lon": 10.1572212 }, + { "lat": 56.1986001, "lon": 10.1579213 }, + { "lat": 56.1986410, "lon": 10.1579414 }, + { "lat": 56.1986175, "lon": 10.1580959 }, + { "lat": 56.1990943, "lon": 10.1583330 }, + { "lat": 56.1991913, "lon": 10.1577163 }, + { "lat": 56.1990920, "lon": 10.1576577 }, + { "lat": 56.1991000, "lon": 10.1576050 }, + { "lat": 56.1989401, "lon": 10.1575264 }, + { "lat": 56.1989477, "lon": 10.1574761 }, + { "lat": 56.1988018, "lon": 10.1574044 }, + { "lat": 56.1988211, "lon": 10.1572774 }, + { "lat": 56.1987065, "lon": 10.1572212 } + ], + "tags": { + "access": "customers", + "amenity": "parking", + "capacity:disabled": "3", + "fee": "no", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 652489484, + "bounds": { + "minlat": 56.2011876, + "minlon": 10.1553895, + "maxlat": 56.2024346, + "maxlon": 10.1579497 + }, + "geometry": [ + { "lat": 56.2021469, "lon": 10.1579497 }, + { "lat": 56.2011876, "lon": 10.1575337 }, + { "lat": 56.2014752, "lon": 10.1553895 }, + { "lat": 56.2015544, "lon": 10.1554238 }, + { "lat": 56.2019505, "lon": 10.1555955 }, + { "lat": 56.2023573, "lon": 10.1557719 }, + { "lat": 56.2024346, "lon": 10.1558054 }, + { "lat": 56.2021469, "lon": 10.1579497 } + ], + "tags": { + "access": "private", + "amenity": "parking", + "capacity:disabled": "3", + "name": "P1", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 678335294, + "bounds": { + "minlat": 56.1698214, + "minlon": 10.1903360, + "maxlat": 56.1701935, + "maxlon": 10.1911530 + }, + "geometry": [ + { "lat": 56.1701935, "lon": 10.1906646 }, + { "lat": 56.1700511, "lon": 10.1906057 }, + { "lat": 56.1700835, "lon": 10.1903741 }, + { "lat": 56.1699973, "lon": 10.1903360 }, + { "lat": 56.1699256, "lon": 10.1908894 }, + { "lat": 56.1698386, "lon": 10.1908513 }, + { "lat": 56.1698214, "lon": 10.1909960 }, + { "lat": 56.1699471, "lon": 10.1910700 }, + { "lat": 56.1701358, "lon": 10.1911530 }, + { "lat": 56.1701935, "lon": 10.1906646 } + ], + "tags": { + "access": "private", + "amenity": "parking", + "capacity": "55", + "capacity:disabled": "3", + "operator:type": "private", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 699224144, + "bounds": { + "minlat": 56.1173365, + "minlon": 10.1525860, + "maxlat": 56.1173888, + "maxlon": 10.1526784 + }, + "geometry": [ + { "lat": 56.1173888, "lon": 10.1526280 }, + { "lat": 56.1173575, "lon": 10.1526784 }, + { "lat": 56.1173365, "lon": 10.1526364 }, + { "lat": 56.1173677, "lon": 10.1525860 }, + { "lat": 56.1173888, "lon": 10.1526280 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224145, + "bounds": { + "minlat": 56.1173986, + "minlon": 10.1527147, + "maxlat": 56.1174509, + "maxlon": 10.1528067 + }, + "geometry": [ + { "lat": 56.1174509, "lon": 10.1527573 }, + { "lat": 56.1174191, "lon": 10.1528067 }, + { "lat": 56.1173986, "lon": 10.1527640 }, + { "lat": 56.1174304, "lon": 10.1527147 }, + { "lat": 56.1174509, "lon": 10.1527573 } + ], + "tags": { + "access": "no", + "amenity": "parking_space", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224146, + "bounds": { + "minlat": 56.1163026, + "minlon": 10.1528478, + "maxlat": 56.1163467, + "maxlon": 10.1529406 + }, + "geometry": [ + { "lat": 56.1163467, "lon": 10.1528661 }, + { "lat": 56.1163327, "lon": 10.1529406 }, + { "lat": 56.1163026, "lon": 10.1529223 }, + { "lat": 56.1163166, "lon": 10.1528478 }, + { "lat": 56.1163467, "lon": 10.1528661 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224148, + "bounds": { + "minlat": 56.1152975, + "minlon": 10.1525459, + "maxlat": 56.1153442, + "maxlon": 10.1526388 + }, + "geometry": [ + { "lat": 56.1153268, "lon": 10.1525459 }, + { "lat": 56.1153442, "lon": 10.1526151 }, + { "lat": 56.1153148, "lon": 10.1526388 }, + { "lat": 56.1152975, "lon": 10.1525696 }, + { "lat": 56.1153188, "lon": 10.1525524 }, + { "lat": 56.1153268, "lon": 10.1525459 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224149, + "bounds": { + "minlat": 56.1153014, + "minlon": 10.1524595, + "maxlat": 56.1153481, + "maxlon": 10.1525524 + }, + "geometry": [ + { "lat": 56.1153307, "lon": 10.1524595 }, + { "lat": 56.1153481, "lon": 10.1525287 }, + { "lat": 56.1153268, "lon": 10.1525459 }, + { "lat": 56.1153188, "lon": 10.1525524 }, + { "lat": 56.1153014, "lon": 10.1524832 }, + { "lat": 56.1153307, "lon": 10.1524595 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224150, + "bounds": { + "minlat": 56.1156235, + "minlon": 10.1519853, + "maxlat": 56.1156750, + "maxlon": 10.1520752 + }, + "geometry": [ + { "lat": 56.1156668, "lon": 10.1519853 }, + { "lat": 56.1156750, "lon": 10.1520600 }, + { "lat": 56.1156316, "lon": 10.1520752 }, + { "lat": 56.1156235, "lon": 10.1520006 }, + { "lat": 56.1156668, "lon": 10.1519853 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224151, + "bounds": { + "minlat": 56.1161422, + "minlon": 10.1516825, + "maxlat": 56.1161949, + "maxlon": 10.1517677 + }, + "geometry": [ + { "lat": 56.1161949, "lon": 10.1516921 }, + { "lat": 56.1161902, "lon": 10.1517677 }, + { "lat": 56.1161422, "lon": 10.1517581 }, + { "lat": 56.1161469, "lon": 10.1516825 }, + { "lat": 56.1161949, "lon": 10.1516921 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224152, + "bounds": { + "minlat": 56.1166830, + "minlon": 10.1516317, + "maxlat": 56.1167245, + "maxlon": 10.1517207 + }, + "geometry": [ + { "lat": 56.1167245, "lon": 10.1516464 }, + { "lat": 56.1167133, "lon": 10.1517207 }, + { "lat": 56.1166830, "lon": 10.1517059 }, + { "lat": 56.1166942, "lon": 10.1516317 }, + { "lat": 56.1167245, "lon": 10.1516464 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224153, + "bounds": { + "minlat": 56.1165360, + "minlon": 10.1518447, + "maxlat": 56.1165824, + "maxlon": 10.1519364 + }, + "geometry": [ + { "lat": 56.1165824, "lon": 10.1518651 }, + { "lat": 56.1165682, "lon": 10.1519364 }, + { "lat": 56.1165577, "lon": 10.1519295 }, + { "lat": 56.1165360, "lon": 10.1519156 }, + { "lat": 56.1165501, "lon": 10.1518447 }, + { "lat": 56.1165824, "lon": 10.1518651 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 699224154, + "bounds": { + "minlat": 56.1165682, + "minlon": 10.1518651, + "maxlat": 56.1166147, + "maxlon": 10.1519587 + }, + "geometry": [ + { "lat": 56.1166147, "lon": 10.1518868 }, + { "lat": 56.1166003, "lon": 10.1519587 }, + { "lat": 56.1165784, "lon": 10.1519434 }, + { "lat": 56.1165682, "lon": 10.1519364 }, + { "lat": 56.1165824, "lon": 10.1518651 }, + { "lat": 56.1166147, "lon": 10.1518868 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 701012881, + "bounds": { + "minlat": 56.2032170, + "minlon": 10.2333359, + "maxlat": 56.2035483, + "maxlon": 10.2335484 + }, + "geometry": [ + { "lat": 56.2034992, "lon": 10.2334981 }, + { "lat": 56.2032170, "lon": 10.2334186 }, + { "lat": 56.2032242, "lon": 10.2333359 }, + { "lat": 56.2035483, "lon": 10.2334271 }, + { "lat": 56.2035476, "lon": 10.2334357 }, + { "lat": 56.2035378, "lon": 10.2335484 }, + { "lat": 56.2034958, "lon": 10.2335366 }, + { "lat": 56.2034992, "lon": 10.2334981 } + ], + "tags": { + "access": "permit", + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "3", + "orientation": "perpendicular", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 701054367, + "bounds": { + "minlat": 56.1875794, + "minlon": 10.1718136, + "maxlat": 56.1882305, + "maxlon": 10.1729161 + }, + "geometry": [ + { "lat": 56.1875794, "lon": 10.1722038 }, + { "lat": 56.1877351, "lon": 10.1729161 }, + { "lat": 56.1880261, "lon": 10.1727458 }, + { "lat": 56.1879611, "lon": 10.1724458 }, + { "lat": 56.1879124, "lon": 10.1724724 }, + { "lat": 56.1879068, "lon": 10.1724529 }, + { "lat": 56.1882305, "lon": 10.1721214 }, + { "lat": 56.1881629, "lon": 10.1718136 }, + { "lat": 56.1878811, "lon": 10.1719420 }, + { "lat": 56.1875912, "lon": 10.1721403 }, + { "lat": 56.1875794, "lon": 10.1722038 } + ], + "tags": { + "amenity": "parking", + "capacity": "67", + "capacity:disabled": "4", + "fee": "yes", + "name": "P5", + "operator": "APCOA Parking", + "operator:type": "private", + "operator:wikidata": "Q296108", + "parking": "surface", + "supervised": "yes", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 713180804, + "bounds": { + "minlat": 56.1722755, + "minlon": 10.1935025, + "maxlat": 56.1726088, + "maxlon": 10.1938461 + }, + "geometry": [ + { "lat": 56.1722755, "lon": 10.1936926 }, + { "lat": 56.1722915, "lon": 10.1935911 }, + { "lat": 56.1723054, "lon": 10.1935025 }, + { "lat": 56.1725326, "lon": 10.1936172 }, + { "lat": 56.1726088, "lon": 10.1936564 }, + { "lat": 56.1725864, "lon": 10.1937986 }, + { "lat": 56.1725824, "lon": 10.1938202 }, + { "lat": 56.1725778, "lon": 10.1938461 }, + { "lat": 56.1724736, "lon": 10.1937904 }, + { "lat": 56.1722755, "lon": 10.1936926 } + ], + "tags": { + "access": "private", + "amenity": "parking", + "capacity": "9", + "capacity:disabled": "1", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 734927543, + "bounds": { + "minlat": 56.1478976, + "minlon": 10.1888081, + "maxlat": 56.1482510, + "maxlon": 10.1912061 + }, + "geometry": [ + { "lat": 56.1480649, "lon": 10.1888081 }, + { "lat": 56.1482510, "lon": 10.1898019 }, + { "lat": 56.1481939, "lon": 10.1898381 }, + { "lat": 56.1480250, "lon": 10.1899367 }, + { "lat": 56.1482444, "lon": 10.1911486 }, + { "lat": 56.1481459, "lon": 10.1912061 }, + { "lat": 56.1481299, "lon": 10.1910961 }, + { "lat": 56.1480732, "lon": 10.1909164 }, + { "lat": 56.1480640, "lon": 10.1908309 }, + { "lat": 56.1480583, "lon": 10.1907775 }, + { "lat": 56.1478976, "lon": 10.1892762 }, + { "lat": 56.1479424, "lon": 10.1892574 }, + { "lat": 56.1479066, "lon": 10.1889114 }, + { "lat": 56.1480649, "lon": 10.1888081 } + ], + "tags": { + "access": "private", + "access:conditional": "yes @ Mo-Su 18:00-06:00", + "amenity": "parking", + "capacity:disabled": "1", + "fee": "no", + "fee:conditional": "yes @ Mo-Su 18:00-06:00", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 949168448, + "bounds": { + "minlat": 56.1502769, + "minlon": 10.0895758, + "maxlat": 56.1505510, + "maxlon": 10.0898644 + }, + "geometry": [ + { "lat": 56.1505436, "lon": 10.0895758 }, + { "lat": 56.1505510, "lon": 10.0898403 }, + { "lat": 56.1502843, "lon": 10.0898644 }, + { "lat": 56.1502769, "lon": 10.0895999 }, + { "lat": 56.1505436, "lon": 10.0895758 } + ], + "tags": { + "amenity": "parking", + "capacity": "21", + "capacity:disabled": "1", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 998382663, + "bounds": { + "minlat": 56.1703246, + "minlon": 10.1757213, + "maxlat": 56.1708360, + "maxlon": 10.1763909 + }, + "geometry": [ + { "lat": 56.1708360, "lon": 10.1763462 }, + { "lat": 56.1708266, "lon": 10.1757213 }, + { "lat": 56.1703246, "lon": 10.1757919 }, + { "lat": 56.1703431, "lon": 10.1763909 }, + { "lat": 56.1708360, "lon": 10.1763462 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "6" + } +}, +{ + "type": "way", + "id": 1007190677, + "bounds": { + "minlat": 56.1549570, + "minlon": 10.2063419, + "maxlat": 56.1552428, + "maxlon": 10.2069895 + }, + "geometry": [ + { "lat": 56.1551116, "lon": 10.2063419 }, + { "lat": 56.1549570, "lon": 10.2068539 }, + { "lat": 56.1550895, "lon": 10.2069895 }, + { "lat": 56.1552428, "lon": 10.2065201 }, + { "lat": 56.1551116, "lon": 10.2063419 } + ], + "tags": { + "amenity": "parking", + "capacity": "20", + "capacity:disabled": "2", + "parking": "surface", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 1009150182, + "bounds": { + "minlat": 56.1702723, + "minlon": 10.2041710, + "maxlat": 56.1704535, + "maxlon": 10.2043689 + }, + "geometry": [ + { "lat": 56.1704328, "lon": 10.2041710 }, + { "lat": 56.1704405, "lon": 10.2041958 }, + { "lat": 56.1704535, "lon": 10.2042379 }, + { "lat": 56.1703750, "lon": 10.2043111 }, + { "lat": 56.1702884, "lon": 10.2043689 }, + { "lat": 56.1702779, "lon": 10.2043163 }, + { "lat": 56.1702723, "lon": 10.2042885 }, + { "lat": 56.1703599, "lon": 10.2042361 }, + { "lat": 56.1704328, "lon": 10.2041710 } + ], + "tags": { + "amenity": "parking", + "capacity": "7", + "capacity:disabled": "1", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1039832836, + "bounds": { + "minlat": 56.2086195, + "minlon": 10.1888993, + "maxlat": 56.2091647, + "maxlon": 10.1892588 + }, + "geometry": [ + { "lat": 56.2091498, "lon": 10.1888993 }, + { "lat": 56.2091565, "lon": 10.1889583 }, + { "lat": 56.2091647, "lon": 10.1890174 }, + { "lat": 56.2086642, "lon": 10.1892588 }, + { "lat": 56.2086195, "lon": 10.1889959 }, + { "lat": 56.2091498, "lon": 10.1888993 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1104381931, + "bounds": { + "minlat": 56.1438502, + "minlon": 10.1987172, + "maxlat": 56.1439623, + "maxlon": 10.1989850 + }, + "geometry": [ + { "lat": 56.1438502, "lon": 10.1987418 }, + { "lat": 56.1439421, "lon": 10.1987172 }, + { "lat": 56.1439491, "lon": 10.1988008 }, + { "lat": 56.1439044, "lon": 10.1988128 }, + { "lat": 56.1439109, "lon": 10.1988902 }, + { "lat": 56.1439555, "lon": 10.1988782 }, + { "lat": 56.1439623, "lon": 10.1989604 }, + { "lat": 56.1438704, "lon": 10.1989850 }, + { "lat": 56.1438502, "lon": 10.1987418 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "1", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1115776184, + "bounds": { + "minlat": 56.1693222, + "minlon": 10.2098274, + "maxlat": 56.1696108, + "maxlon": 10.2110728 + }, + "geometry": [ + { "lat": 56.1695669, "lon": 10.2110728 }, + { "lat": 56.1696108, "lon": 10.2110443 }, + { "lat": 56.1693663, "lon": 10.2098274 }, + { "lat": 56.1693222, "lon": 10.2098559 }, + { "lat": 56.1695669, "lon": 10.2110728 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "30", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1120805283, + "bounds": { + "minlat": 56.1765425, + "minlon": 10.1346966, + "maxlat": 56.1769428, + "maxlon": 10.1364715 + }, + "geometry": [ + { "lat": 56.1769428, "lon": 10.1361715 }, + { "lat": 56.1769339, "lon": 10.1364715 }, + { "lat": 56.1765425, "lon": 10.1364325 }, + { "lat": 56.1765448, "lon": 10.1362689 }, + { "lat": 56.1765467, "lon": 10.1362001 }, + { "lat": 56.1765483, "lon": 10.1361239 }, + { "lat": 56.1765770, "lon": 10.1361258 }, + { "lat": 56.1766075, "lon": 10.1354455 }, + { "lat": 56.1766534, "lon": 10.1354547 }, + { "lat": 56.1766917, "lon": 10.1346966 }, + { "lat": 56.1768625, "lon": 10.1347264 }, + { "lat": 56.1768395, "lon": 10.1353196 }, + { "lat": 56.1767529, "lon": 10.1353218 }, + { "lat": 56.1767312, "lon": 10.1361463 }, + { "lat": 56.1769428, "lon": 10.1361715 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity:charging": "1", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1145422964, + "bounds": { + "minlat": 56.1848960, + "minlon": 10.1874250, + "maxlat": 56.1850784, + "maxlon": 10.1878028 + }, + "geometry": [ + { "lat": 56.1850784, "lon": 10.1876087 }, + { "lat": 56.1850201, "lon": 10.1876531 }, + { "lat": 56.1850409, "lon": 10.1877413 }, + { "lat": 56.1849601, "lon": 10.1878028 }, + { "lat": 56.1848960, "lon": 10.1875309 }, + { "lat": 56.1850352, "lon": 10.1874250 }, + { "lat": 56.1850784, "lon": 10.1876087 } + ], + "tags": { + "access": "private", + "amenity": "parking", + "capacity": "9", + "capacity:disabled": "1", + "parking": "surface", + "smoothness": "excellent", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1169932402, + "bounds": { + "minlat": 56.1491675, + "minlon": 10.2058894, + "maxlat": 56.1492021, + "maxlon": 10.2059728 + }, + "geometry": [ + { "lat": 56.1492021, "lon": 10.2058984 }, + { "lat": 56.1491944, "lon": 10.2059728 }, + { "lat": 56.1491675, "lon": 10.2059638 }, + { "lat": 56.1491753, "lon": 10.2058894 }, + { "lat": 56.1492021, "lon": 10.2058984 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1169932403, + "bounds": { + "minlat": 56.1491287, + "minlon": 10.2058750, + "maxlat": 56.1491633, + "maxlon": 10.2059584 + }, + "geometry": [ + { "lat": 56.1491633, "lon": 10.2058840 }, + { "lat": 56.1491555, "lon": 10.2059584 }, + { "lat": 56.1491287, "lon": 10.2059494 }, + { "lat": 56.1491365, "lon": 10.2058750 }, + { "lat": 56.1491633, "lon": 10.2058840 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1170983458, + "bounds": { + "minlat": 56.1865522, + "minlon": 10.1876642, + "maxlat": 56.1868423, + "maxlon": 10.1885168 + }, + "geometry": [ + { "lat": 56.1866631, "lon": 10.1876743 }, + { "lat": 56.1866351, "lon": 10.1876642 }, + { "lat": 56.1865522, "lon": 10.1877252 }, + { "lat": 56.1865696, "lon": 10.1878028 }, + { "lat": 56.1865946, "lon": 10.1879097 }, + { "lat": 56.1867365, "lon": 10.1885168 }, + { "lat": 56.1868314, "lon": 10.1884452 }, + { "lat": 56.1868423, "lon": 10.1884230 }, + { "lat": 56.1868266, "lon": 10.1883597 }, + { "lat": 56.1867486, "lon": 10.1880340 }, + { "lat": 56.1867129, "lon": 10.1878833 }, + { "lat": 56.1866631, "lon": 10.1876743 } + ], + "tags": { + "amenity": "parking", + "capacity:charging": "4", + "capacity:disabled": "3" + } +}, +{ + "type": "way", + "id": 1175981984, + "bounds": { + "minlat": 56.1702113, + "minlon": 10.1638707, + "maxlat": 56.1705444, + "maxlon": 10.1641555 + }, + "geometry": [ + { "lat": 56.1705444, "lon": 10.1641555 }, + { "lat": 56.1702113, "lon": 10.1640314 }, + { "lat": 56.1702148, "lon": 10.1638707 }, + { "lat": 56.1705408, "lon": 10.1639057 }, + { "lat": 56.1705444, "lon": 10.1641555 } + ], + "tags": { + "amenity": "parking", + "capacity": "16", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1180104359, + "bounds": { + "minlat": 56.1904120, + "minlon": 10.1755838, + "maxlat": 56.1904742, + "maxlon": 10.1757281 + }, + "geometry": [ + { "lat": 56.1904492, "lon": 10.1755838 }, + { "lat": 56.1904120, "lon": 10.1756099 }, + { "lat": 56.1904377, "lon": 10.1757281 }, + { "lat": 56.1904742, "lon": 10.1757025 }, + { "lat": 56.1904492, "lon": 10.1755838 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180104360, + "bounds": { + "minlat": 56.1904871, + "minlon": 10.1755302, + "maxlat": 56.1905511, + "maxlon": 10.1756761 + }, + "geometry": [ + { "lat": 56.1905117, "lon": 10.1756761 }, + { "lat": 56.1905511, "lon": 10.1756484 }, + { "lat": 56.1905254, "lon": 10.1755302 }, + { "lat": 56.1904871, "lon": 10.1755571 }, + { "lat": 56.1905117, "lon": 10.1756761 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180104362, + "bounds": { + "minlat": 56.1904492, + "minlon": 10.1755571, + "maxlat": 56.1905117, + "maxlon": 10.1757025 + }, + "geometry": [ + { "lat": 56.1904742, "lon": 10.1757025 }, + { "lat": 56.1905117, "lon": 10.1756761 }, + { "lat": 56.1904871, "lon": 10.1755571 }, + { "lat": 56.1904492, "lon": 10.1755838 }, + { "lat": 56.1904742, "lon": 10.1757025 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180104373, + "bounds": { + "minlat": 56.1886085, + "minlon": 10.1739903, + "maxlat": 56.1886567, + "maxlon": 10.1740560 + }, + "geometry": [ + { "lat": 56.1886529, "lon": 10.1739903 }, + { "lat": 56.1886567, "lon": 10.1740251 }, + { "lat": 56.1886160, "lon": 10.1740560 }, + { "lat": 56.1886085, "lon": 10.1740245 }, + { "lat": 56.1886529, "lon": 10.1739903 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180104374, + "bounds": { + "minlat": 56.1886529, + "minlon": 10.1739849, + "maxlat": 56.1887003, + "maxlon": 10.1740251 + }, + "geometry": [ + { "lat": 56.1886529, "lon": 10.1739903 }, + { "lat": 56.1886992, "lon": 10.1739849 }, + { "lat": 56.1887003, "lon": 10.1740178 }, + { "lat": 56.1886567, "lon": 10.1740251 }, + { "lat": 56.1886529, "lon": 10.1739903 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180104385, + "bounds": { + "minlat": 56.1902321, + "minlon": 10.1736588, + "maxlat": 56.1902943, + "maxlon": 10.1737553 + }, + "geometry": [ + { "lat": 56.1902810, "lon": 10.1736588 }, + { "lat": 56.1902943, "lon": 10.1737189 }, + { "lat": 56.1902453, "lon": 10.1737553 }, + { "lat": 56.1902321, "lon": 10.1736935 }, + { "lat": 56.1902810, "lon": 10.1736588 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180104386, + "bounds": { + "minlat": 56.1902190, + "minlon": 10.1735993, + "maxlat": 56.1902810, + "maxlon": 10.1736935 + }, + "geometry": [ + { "lat": 56.1902321, "lon": 10.1736935 }, + { "lat": 56.1902190, "lon": 10.1736322 }, + { "lat": 56.1902678, "lon": 10.1735993 }, + { "lat": 56.1902810, "lon": 10.1736588 }, + { "lat": 56.1902321, "lon": 10.1736935 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180104390, + "bounds": { + "minlat": 56.1902440, + "minlon": 10.1750766, + "maxlat": 56.1902966, + "maxlon": 10.1751424 + }, + "geometry": [ + { "lat": 56.1902875, "lon": 10.1750766 }, + { "lat": 56.1902966, "lon": 10.1751188 }, + { "lat": 56.1902580, "lon": 10.1751424 }, + { "lat": 56.1902524, "lon": 10.1751163 }, + { "lat": 56.1902440, "lon": 10.1751054 }, + { "lat": 56.1902875, "lon": 10.1750766 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180544298, + "bounds": { + "minlat": 56.1832145, + "minlon": 10.1061762, + "maxlat": 56.1832532, + "maxlon": 10.1063419 + }, + "geometry": [ + { "lat": 56.1832145, "lon": 10.1061762 }, + { "lat": 56.1832532, "lon": 10.1061762 }, + { "lat": 56.1832532, "lon": 10.1063419 }, + { "lat": 56.1832145, "lon": 10.1063419 }, + { "lat": 56.1832145, "lon": 10.1061762 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "3", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180544299, + "bounds": { + "minlat": 56.1831101, + "minlon": 10.1061743, + "maxlat": 56.1831465, + "maxlon": 10.1063439 + }, + "geometry": [ + { "lat": 56.1831457, "lon": 10.1061743 }, + { "lat": 56.1831465, "lon": 10.1063434 }, + { "lat": 56.1831108, "lon": 10.1063439 }, + { "lat": 56.1831101, "lon": 10.1061748 }, + { "lat": 56.1831457, "lon": 10.1061743 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "3", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180544300, + "bounds": { + "minlat": 56.1830360, + "minlon": 10.1034893, + "maxlat": 56.1831260, + "maxlon": 10.1036004 + }, + "geometry": [ + { "lat": 56.1830360, "lon": 10.1034912 }, + { "lat": 56.1831253, "lon": 10.1034893 }, + { "lat": 56.1831260, "lon": 10.1035985 }, + { "lat": 56.1830367, "lon": 10.1036004 }, + { "lat": 56.1830360, "lon": 10.1034912 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "4", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180544301, + "bounds": { + "minlat": 56.1827321, + "minlon": 10.1034940, + "maxlat": 56.1828196, + "maxlon": 10.1036026 + }, + "geometry": [ + { "lat": 56.1828196, "lon": 10.1034941 }, + { "lat": 56.1828196, "lon": 10.1036026 }, + { "lat": 56.1827321, "lon": 10.1036025 }, + { "lat": 56.1827321, "lon": 10.1034940 }, + { "lat": 56.1828196, "lon": 10.1034941 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "4", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180544611, + "bounds": { + "minlat": 56.1834502, + "minlon": 10.1005759, + "maxlat": 56.1835552, + "maxlon": 10.1007346 + }, + "geometry": [ + { "lat": 56.1834502, "lon": 10.1006868 }, + { "lat": 56.1834693, "lon": 10.1005759 }, + { "lat": 56.1835552, "lon": 10.1006236 }, + { "lat": 56.1835362, "lon": 10.1007346 }, + { "lat": 56.1834502, "lon": 10.1006868 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "4", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180544707, + "bounds": { + "minlat": 56.1806748, + "minlon": 10.0969319, + "maxlat": 56.1807434, + "maxlon": 10.0970614 + }, + "geometry": [ + { "lat": 56.1806748, "lon": 10.0969762 }, + { "lat": 56.1807116, "lon": 10.0970614 }, + { "lat": 56.1807434, "lon": 10.0970171 }, + { "lat": 56.1807067, "lon": 10.0969319 }, + { "lat": 56.1806748, "lon": 10.0969762 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1180924175, + "bounds": { + "minlat": 56.1661251, + "minlon": 10.2118232, + "maxlat": 56.1662083, + "maxlon": 10.2120810 + }, + "geometry": [ + { "lat": 56.1661251, "lon": 10.2120571 }, + { "lat": 56.1661659, "lon": 10.2118232 }, + { "lat": 56.1662083, "lon": 10.2118471 }, + { "lat": 56.1661674, "lon": 10.2120810 }, + { "lat": 56.1661251, "lon": 10.2120571 } + ], + "tags": { + "amenity": "parking", + "capacity": "5", + "capacity:disabled": "5", + "orientation": "perpendicular", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1180925043, + "bounds": { + "minlat": 56.1561151, + "minlon": 10.2102936, + "maxlat": 56.1561991, + "maxlon": 10.2105437 + }, + "geometry": [ + { "lat": 56.1561298, "lon": 10.2102936 }, + { "lat": 56.1561991, "lon": 10.2105298 }, + { "lat": 56.1561845, "lon": 10.2105437 }, + { "lat": 56.1561151, "lon": 10.2103074 }, + { "lat": 56.1561298, "lon": 10.2102936 } + ], + "tags": { + "amenity": "parking", + "capacity": "3", + "capacity:disabled": "3", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1180925044, + "bounds": { + "minlat": 56.1562885, + "minlon": 10.2108831, + "maxlat": 56.1564034, + "maxlon": 10.2112349 + }, + "geometry": [ + { "lat": 56.1563868, "lon": 10.2112349 }, + { "lat": 56.1564034, "lon": 10.2112193 }, + { "lat": 56.1563050, "lon": 10.2108831 }, + { "lat": 56.1562885, "lon": 10.2108986 }, + { "lat": 56.1563868, "lon": 10.2112349 } + ], + "tags": { + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "4", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1180931401, + "bounds": { + "minlat": 56.1528614, + "minlon": 10.2113011, + "maxlat": 56.1529113, + "maxlon": 10.2113765 + }, + "geometry": [ + { "lat": 56.1528614, "lon": 10.2113520 }, + { "lat": 56.1529017, "lon": 10.2113765 }, + { "lat": 56.1529113, "lon": 10.2113256 }, + { "lat": 56.1528710, "lon": 10.2113011 }, + { "lat": 56.1528614, "lon": 10.2113520 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1181267216, + "bounds": { + "minlat": 56.1936877, + "minlon": 10.2079778, + "maxlat": 56.1937359, + "maxlon": 10.2080537 + }, + "geometry": [ + { "lat": 56.1937359, "lon": 10.2080045 }, + { "lat": 56.1937250, "lon": 10.2080537 }, + { "lat": 56.1936877, "lon": 10.2080271 }, + { "lat": 56.1936985, "lon": 10.2079778 }, + { "lat": 56.1937359, "lon": 10.2080045 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1181267217, + "bounds": { + "minlat": 56.1936557, + "minlon": 10.2080626, + "maxlat": 56.1937165, + "maxlon": 10.2081918 + }, + "geometry": [ + { "lat": 56.1937165, "lon": 10.2080905 }, + { "lat": 56.1936934, "lon": 10.2081918 }, + { "lat": 56.1936557, "lon": 10.2081639 }, + { "lat": 56.1936788, "lon": 10.2080626 }, + { "lat": 56.1937165, "lon": 10.2080905 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1181267218, + "bounds": { + "minlat": 56.1937320, + "minlon": 10.2080314, + "maxlat": 56.1937812, + "maxlon": 10.2081109 + }, + "geometry": [ + { "lat": 56.1937812, "lon": 10.2080587 }, + { "lat": 56.1937693, "lon": 10.2081109 }, + { "lat": 56.1937320, "lon": 10.2080836 }, + { "lat": 56.1937438, "lon": 10.2080314 }, + { "lat": 56.1937812, "lon": 10.2080587 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1181267219, + "bounds": { + "minlat": 56.1937066, + "minlon": 10.2081106, + "maxlat": 56.1937647, + "maxlon": 10.2082270 + }, + "geometry": [ + { "lat": 56.1937647, "lon": 10.2081384 }, + { "lat": 56.1937447, "lon": 10.2082270 }, + { "lat": 56.1937066, "lon": 10.2081993 }, + { "lat": 56.1937266, "lon": 10.2081106 }, + { "lat": 56.1937647, "lon": 10.2081384 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1181861773, + "bounds": { + "minlat": 56.1522237, + "minlon": 10.1985809, + "maxlat": 56.1522839, + "maxlon": 10.1986941 + }, + "geometry": [ + { "lat": 56.1522237, "lon": 10.1986520 }, + { "lat": 56.1522392, "lon": 10.1985809 }, + { "lat": 56.1522839, "lon": 10.1985912 }, + { "lat": 56.1522716, "lon": 10.1986941 }, + { "lat": 56.1522237, "lon": 10.1986520 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1181944200, + "bounds": { + "minlat": 56.2036810, + "minlon": 10.1802931, + "maxlat": 56.2037899, + "maxlon": 10.1804589 + }, + "geometry": [ + { "lat": 56.2037657, "lon": 10.1802931 }, + { "lat": 56.2037899, "lon": 10.1803928 }, + { "lat": 56.2037049, "lon": 10.1804589 }, + { "lat": 56.2036810, "lon": 10.1803593 }, + { "lat": 56.2037657, "lon": 10.1802931 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "6", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1181944202, + "bounds": { + "minlat": 56.2032843, + "minlon": 10.1791633, + "maxlat": 56.2034175, + "maxlon": 10.1795925 + }, + "geometry": [ + { "lat": 56.2032843, "lon": 10.1791903 }, + { "lat": 56.2033180, "lon": 10.1791633 }, + { "lat": 56.2034175, "lon": 10.1795655 }, + { "lat": 56.2033837, "lon": 10.1795925 }, + { "lat": 56.2032843, "lon": 10.1791903 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1185046743, + "bounds": { + "minlat": 56.1507024, + "minlon": 10.2529067, + "maxlat": 56.1507889, + "maxlon": 10.2530957 + }, + "geometry": [ + { "lat": 56.1507024, "lon": 10.2529479 }, + { "lat": 56.1507425, "lon": 10.2530957 }, + { "lat": 56.1507889, "lon": 10.2530541 }, + { "lat": 56.1507501, "lon": 10.2529067 }, + { "lat": 56.1507024, "lon": 10.2529479 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "3", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1185046744, + "bounds": { + "minlat": 56.1507505, + "minlon": 10.2530838, + "maxlat": 56.1508096, + "maxlon": 10.2531729 + }, + "geometry": [ + { "lat": 56.1507505, "lon": 10.2531237 }, + { "lat": 56.1507632, "lon": 10.2531729 }, + { "lat": 56.1508096, "lon": 10.2531325 }, + { "lat": 56.1507967, "lon": 10.2530838 }, + { "lat": 56.1507505, "lon": 10.2531237 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1204431304, + "bounds": { + "minlat": 56.1544159, + "minlon": 10.1910357, + "maxlat": 56.1545280, + "maxlon": 10.1912194 + }, + "geometry": [ + { "lat": 56.1545242, "lon": 10.1910357 }, + { "lat": 56.1545280, "lon": 10.1911202 }, + { "lat": 56.1544398, "lon": 10.1912194 }, + { "lat": 56.1544159, "lon": 10.1911511 }, + { "lat": 56.1545242, "lon": 10.1910357 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1209208135, + "bounds": { + "minlat": 56.2187169, + "minlon": 10.1672796, + "maxlat": 56.2188105, + "maxlon": 10.1674640 + }, + "geometry": [ + { "lat": 56.2187714, "lon": 10.1673072 }, + { "lat": 56.2187169, "lon": 10.1672796 }, + { "lat": 56.2187531, "lon": 10.1674293 }, + { "lat": 56.2188105, "lon": 10.1674640 }, + { "lat": 56.2187714, "lon": 10.1673072 } + ], + "tags": { + "access": "customers", + "amenity": "parking", + "capacity:disabled": "3", + "fee": "no", + "parking": "surface", + "surface": "paving_stones" + } +}, +{ + "type": "way", + "id": 1225703643, + "bounds": { + "minlat": 56.1439765, + "minlon": 10.1982786, + "maxlat": 56.1440550, + "maxlon": 10.1983938 + }, + "geometry": [ + { "lat": 56.1439765, "lon": 10.1982986 }, + { "lat": 56.1439834, "lon": 10.1983938 }, + { "lat": 56.1440550, "lon": 10.1983774 }, + { "lat": 56.1440481, "lon": 10.1982786 }, + { "lat": 56.1439765, "lon": 10.1982986 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1246239955, + "bounds": { + "minlat": 56.0887645, + "minlon": 10.2479635, + "maxlat": 56.0888243, + "maxlon": 10.2480684 + }, + "geometry": [ + { "lat": 56.0888243, "lon": 10.2479955 }, + { "lat": 56.0887829, "lon": 10.2479635 }, + { "lat": 56.0887789, "lon": 10.2479722 }, + { "lat": 56.0887748, "lon": 10.2479786 }, + { "lat": 56.0887697, "lon": 10.2479843 }, + { "lat": 56.0887645, "lon": 10.2479884 }, + { "lat": 56.0887778, "lon": 10.2480684 }, + { "lat": 56.0887850, "lon": 10.2480633 }, + { "lat": 56.0887972, "lon": 10.2480511 }, + { "lat": 56.0888079, "lon": 10.2480355 }, + { "lat": 56.0888174, "lon": 10.2480159 }, + { "lat": 56.0888243, "lon": 10.2479955 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1249246264, + "bounds": { + "minlat": 56.1526125, + "minlon": 10.2074858, + "maxlat": 56.1527393, + "maxlon": 10.2080231 + }, + "geometry": [ + { "lat": 56.1527159, "lon": 10.2074858 }, + { "lat": 56.1526125, "lon": 10.2080082 }, + { "lat": 56.1526360, "lon": 10.2080231 }, + { "lat": 56.1527393, "lon": 10.2075008 }, + { "lat": 56.1527159, "lon": 10.2074858 } + ], + "tags": { + "amenity": "parking", + "capacity": "5", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1252141576, + "bounds": { + "minlat": 56.1864794, + "minlon": 10.2266862, + "maxlat": 56.1865262, + "maxlon": 10.2268226 + }, + "geometry": [ + { "lat": 56.1864794, "lon": 10.2268168 }, + { "lat": 56.1865262, "lon": 10.2268226 }, + { "lat": 56.1865125, "lon": 10.2266897 }, + { "lat": 56.1864853, "lon": 10.2266862 }, + { "lat": 56.1864794, "lon": 10.2268168 } + ], + "tags": { + "access": "unknown", + "amenity": "parking", + "capacity": "0", + "capacity:disabled": "1", + "parking": "surface", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1259782429, + "bounds": { + "minlat": 56.0373328, + "minlon": 10.0832138, + "maxlat": 56.0374962, + "maxlon": 10.0836673 + }, + "geometry": [ + { "lat": 56.0374962, "lon": 10.0832669 }, + { "lat": 56.0374395, "lon": 10.0835662 }, + { "lat": 56.0374296, "lon": 10.0836188 }, + { "lat": 56.0374204, "lon": 10.0836673 }, + { "lat": 56.0373328, "lon": 10.0836142 }, + { "lat": 56.0374086, "lon": 10.0832138 }, + { "lat": 56.0374962, "lon": 10.0832669 } + ], + "tags": { + "amenity": "parking", + "capacity:charging": "2", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1263319626, + "bounds": { + "minlat": 56.1664851, + "minlon": 10.2260670, + "maxlat": 56.1665739, + "maxlon": 10.2261958 + }, + "geometry": [ + { "lat": 56.1665584, "lon": 10.2260670 }, + { "lat": 56.1665739, "lon": 10.2261097 }, + { "lat": 56.1665007, "lon": 10.2261958 }, + { "lat": 56.1664851, "lon": 10.2261531 }, + { "lat": 56.1665584, "lon": 10.2260670 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "fee": "no", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1263320059, + "bounds": { + "minlat": 56.1662936, + "minlon": 10.2263448, + "maxlat": 56.1663429, + "maxlon": 10.2264258 + }, + "geometry": [ + { "lat": 56.1663282, "lon": 10.2263448 }, + { "lat": 56.1663429, "lon": 10.2263846 }, + { "lat": 56.1663083, "lon": 10.2264258 }, + { "lat": 56.1662936, "lon": 10.2263859 }, + { "lat": 56.1663282, "lon": 10.2263448 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "fee": "no" + } +}, +{ + "type": "way", + "id": 1263320853, + "bounds": { + "minlat": 56.1664269, + "minlon": 10.2259661, + "maxlat": 56.1664753, + "maxlon": 10.2260459 + }, + "geometry": [ + { "lat": 56.1664753, "lon": 10.2260031 }, + { "lat": 56.1664413, "lon": 10.2260459 }, + { "lat": 56.1664269, "lon": 10.2260089 }, + { "lat": 56.1664608, "lon": 10.2259661 }, + { "lat": 56.1664753, "lon": 10.2260031 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1263320854, + "bounds": { + "minlat": 56.1663700, + "minlon": 10.2260334, + "maxlat": 56.1664184, + "maxlon": 10.2261132 + }, + "geometry": [ + { "lat": 56.1664184, "lon": 10.2260704 }, + { "lat": 56.1663844, "lon": 10.2261132 }, + { "lat": 56.1663700, "lon": 10.2260762 }, + { "lat": 56.1664039, "lon": 10.2260334 }, + { "lat": 56.1664184, "lon": 10.2260704 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1263320855, + "bounds": { + "minlat": 56.1663131, + "minlon": 10.2261007, + "maxlat": 56.1663615, + "maxlon": 10.2261804 + }, + "geometry": [ + { "lat": 56.1663615, "lon": 10.2261376 }, + { "lat": 56.1663275, "lon": 10.2261804 }, + { "lat": 56.1663131, "lon": 10.2261435 }, + { "lat": 56.1663470, "lon": 10.2261007 }, + { "lat": 56.1663615, "lon": 10.2261376 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1263320856, + "bounds": { + "minlat": 56.1661654, + "minlon": 10.2262812, + "maxlat": 56.1662139, + "maxlon": 10.2263610 + }, + "geometry": [ + { "lat": 56.1662139, "lon": 10.2263182 }, + { "lat": 56.1661799, "lon": 10.2263610 }, + { "lat": 56.1661654, "lon": 10.2263240 }, + { "lat": 56.1661994, "lon": 10.2262812 }, + { "lat": 56.1662139, "lon": 10.2263182 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1263321180, + "bounds": { + "minlat": 56.1680824, + "minlon": 10.2186845, + "maxlat": 56.1681187, + "maxlon": 10.2187272 + }, + "geometry": [ + { "lat": 56.1681187, "lon": 10.2186994 }, + { "lat": 56.1681147, "lon": 10.2187272 }, + { "lat": 56.1680824, "lon": 10.2187122 }, + { "lat": 56.1680864, "lon": 10.2186845 }, + { "lat": 56.1681187, "lon": 10.2186994 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1263321181, + "bounds": { + "minlat": 56.1680253, + "minlon": 10.2186619, + "maxlat": 56.1680615, + "maxlon": 10.2187046 + }, + "geometry": [ + { "lat": 56.1680615, "lon": 10.2186769 }, + { "lat": 56.1680575, "lon": 10.2187046 }, + { "lat": 56.1680253, "lon": 10.2186897 }, + { "lat": 56.1680293, "lon": 10.2186619 }, + { "lat": 56.1680615, "lon": 10.2186769 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1263321182, + "bounds": { + "minlat": 56.1679825, + "minlon": 10.2186422, + "maxlat": 56.1680188, + "maxlon": 10.2186849 + }, + "geometry": [ + { "lat": 56.1680188, "lon": 10.2186572 }, + { "lat": 56.1680148, "lon": 10.2186849 }, + { "lat": 56.1679825, "lon": 10.2186700 }, + { "lat": 56.1679865, "lon": 10.2186422 }, + { "lat": 56.1680188, "lon": 10.2186572 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "lane" + } +}, +{ + "type": "way", + "id": 1263523967, + "bounds": { + "minlat": 56.1507628, + "minlon": 10.2059609, + "maxlat": 56.1508098, + "maxlon": 10.2061342 + }, + "geometry": [ + { "lat": 56.1507841, "lon": 10.2059609 }, + { "lat": 56.1507628, "lon": 10.2059718 }, + { "lat": 56.1507884, "lon": 10.2061342 }, + { "lat": 56.1508098, "lon": 10.2061233 }, + { "lat": 56.1507841, "lon": 10.2059609 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1263523976, + "bounds": { + "minlat": 56.1540840, + "minlon": 10.2138289, + "maxlat": 56.1541608, + "maxlon": 10.2141838 + }, + "geometry": [ + { "lat": 56.1541081, "lon": 10.2138289 }, + { "lat": 56.1541608, "lon": 10.2141718 }, + { "lat": 56.1541367, "lon": 10.2141838 }, + { "lat": 56.1540840, "lon": 10.2138408 }, + { "lat": 56.1541081, "lon": 10.2138289 } + ], + "tags": { + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "1", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1263523981, + "bounds": { + "minlat": 56.1554816, + "minlon": 10.2110701, + "maxlat": 56.1555422, + "maxlon": 10.2111850 + }, + "geometry": [ + { "lat": 56.1555422, "lon": 10.2110999 }, + { "lat": 56.1554967, "lon": 10.2111850 }, + { "lat": 56.1554816, "lon": 10.2111587 }, + { "lat": 56.1555251, "lon": 10.2110701 }, + { "lat": 56.1555422, "lon": 10.2110999 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1263523986, + "bounds": { + "minlat": 56.1668299, + "minlon": 10.2254496, + "maxlat": 56.1670946, + "maxlon": 10.2257971 + }, + "geometry": [ + { "lat": 56.1670946, "lon": 10.2255091 }, + { "lat": 56.1670768, "lon": 10.2254496 }, + { "lat": 56.1670137, "lon": 10.2255241 }, + { "lat": 56.1669549, "lon": 10.2255935 }, + { "lat": 56.1668949, "lon": 10.2256642 }, + { "lat": 56.1668299, "lon": 10.2257409 }, + { "lat": 56.1668485, "lon": 10.2257971 }, + { "lat": 56.1669131, "lon": 10.2257215 }, + { "lat": 56.1669727, "lon": 10.2256517 }, + { "lat": 56.1670322, "lon": 10.2255803 }, + { "lat": 56.1670946, "lon": 10.2255091 } + ], + "tags": { + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "4", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1263980653, + "bounds": { + "minlat": 56.1548169, + "minlon": 10.1811537, + "maxlat": 56.1548771, + "maxlon": 10.1823870 + }, + "geometry": [ + { "lat": 56.1548732, "lon": 10.1811584 }, + { "lat": 56.1548771, "lon": 10.1823864 }, + { "lat": 56.1548253, "lon": 10.1823870 }, + { "lat": 56.1548185, "lon": 10.1813468 }, + { "lat": 56.1548169, "lon": 10.1811537 }, + { "lat": 56.1548732, "lon": 10.1811584 } + ], + "tags": { + "amenity": "parking", + "capacity": "32", + "capacity:disabled": "1", + "orientation": "perpendicular", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1280358874, + "bounds": { + "minlat": 56.1702403, + "minlon": 10.1925913, + "maxlat": 56.1702963, + "maxlon": 10.1926792 + }, + "geometry": [ + { "lat": 56.1702829, "lon": 10.1925913 }, + { "lat": 56.1702403, "lon": 10.1926447 }, + { "lat": 56.1702538, "lon": 10.1926792 }, + { "lat": 56.1702963, "lon": 10.1926258 }, + { "lat": 56.1702829, "lon": 10.1925913 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1280362581, + "bounds": { + "minlat": 56.1710630, + "minlon": 10.1928037, + "maxlat": 56.1711454, + "maxlon": 10.1928989 + }, + "geometry": [ + { "lat": 56.1711454, "lon": 10.1928421 }, + { "lat": 56.1710722, "lon": 10.1928037 }, + { "lat": 56.1710630, "lon": 10.1928605 }, + { "lat": 56.1711361, "lon": 10.1928989 }, + { "lat": 56.1711454, "lon": 10.1928421 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1283539020, + "bounds": { + "minlat": 56.1848278, + "minlon": 10.2125609, + "maxlat": 56.1849195, + "maxlon": 10.2127216 + }, + "geometry": [ + { "lat": 56.1849195, "lon": 10.2125872 }, + { "lat": 56.1849054, "lon": 10.2127216 }, + { "lat": 56.1848747, "lon": 10.2127168 }, + { "lat": 56.1848278, "lon": 10.2127095 }, + { "lat": 56.1848424, "lon": 10.2125609 }, + { "lat": 56.1849195, "lon": 10.2125872 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "3", + "capacity:disabled": "1", + "fee": "yes", + "operator": "Apkoa", + "operator:type": "private", + "parking": "surface", + "payment:app": "yes", + "surface": "paving_stones" + } +}, +{ + "type": "way", + "id": 1283539021, + "bounds": { + "minlat": 56.1846798, + "minlon": 10.2124679, + "maxlat": 56.1848210, + "maxlon": 10.2126933 + }, + "geometry": [ + { "lat": 56.1848210, "lon": 10.2125498 }, + { "lat": 56.1847822, "lon": 10.2126933 }, + { "lat": 56.1847294, "lon": 10.2126516 }, + { "lat": 56.1846798, "lon": 10.2126124 }, + { "lat": 56.1847192, "lon": 10.2124679 }, + { "lat": 56.1848210, "lon": 10.2125498 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "5", + "capacity:disabled": "1", + "fee": "yes", + "operator": "Apkoa", + "operator:type": "private", + "parking": "surface", + "payment:app": "yes", + "surface": "paving_stones" + } +}, +{ + "type": "way", + "id": 1283539023, + "bounds": { + "minlat": 56.1844127, + "minlon": 10.2122819, + "maxlat": 56.1845122, + "maxlon": 10.2126347 + }, + "geometry": [ + { "lat": 56.1844127, "lon": 10.2126003 }, + { "lat": 56.1844712, "lon": 10.2126347 }, + { "lat": 56.1845122, "lon": 10.2123173 }, + { "lat": 56.1844537, "lon": 10.2122819 }, + { "lat": 56.1844127, "lon": 10.2126003 } + ], + "tags": { + "access": "private", + "amenity": "parking", + "capacity:disabled": "8", + "operator": "Apkoa", + "operator:type": "private", + "parking": "surface", + "surface": "paving_stones" + } +}, +{ + "type": "way", + "id": 1296711528, + "bounds": { + "minlat": 56.1727718, + "minlon": 10.1915727, + "maxlat": 56.1729585, + "maxlon": 10.1918643 + }, + "geometry": [ + { "lat": 56.1729585, "lon": 10.1916371 }, + { "lat": 56.1728014, "lon": 10.1915727 }, + { "lat": 56.1727718, "lon": 10.1917962 }, + { "lat": 56.1727857, "lon": 10.1918015 }, + { "lat": 56.1728253, "lon": 10.1918180 }, + { "lat": 56.1728902, "lon": 10.1918469 }, + { "lat": 56.1729295, "lon": 10.1918643 }, + { "lat": 56.1729585, "lon": 10.1916371 } + ], + "tags": { + "access": "customers", + "amenity": "parking", + "capacity": "10", + "capacity:disabled": "1", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1307490572, + "bounds": { + "minlat": 56.1689157, + "minlon": 10.1168083, + "maxlat": 56.1690397, + "maxlon": 10.1172522 + }, + "geometry": [ + { "lat": 56.1689497, "lon": 10.1168150 }, + { "lat": 56.1689157, "lon": 10.1168592 }, + { "lat": 56.1689284, "lon": 10.1172522 }, + { "lat": 56.1690397, "lon": 10.1172388 }, + { "lat": 56.1690270, "lon": 10.1168083 }, + { "lat": 56.1689497, "lon": 10.1168150 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "2", + "fee": "no", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1322856613, + "bounds": { + "minlat": 56.2374649, + "minlon": 10.2327826, + "maxlat": 56.2375170, + "maxlon": 10.2328924 + }, + "geometry": [ + { "lat": 56.2375170, "lon": 10.2328688 }, + { "lat": 56.2374801, "lon": 10.2328924 }, + { "lat": 56.2374649, "lon": 10.2328036 }, + { "lat": 56.2374996, "lon": 10.2327826 }, + { "lat": 56.2375170, "lon": 10.2328688 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1324193153, + "bounds": { + "minlat": 56.2312387, + "minlon": 10.2353339, + "maxlat": 56.2313240, + "maxlon": 10.2354631 + }, + "geometry": [ + { "lat": 56.2312571, "lon": 10.2354631 }, + { "lat": 56.2313240, "lon": 10.2354091 }, + { "lat": 56.2313054, "lon": 10.2353339 }, + { "lat": 56.2312387, "lon": 10.2353857 }, + { "lat": 56.2312571, "lon": 10.2354631 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1324193176, + "bounds": { + "minlat": 56.2311810, + "minlon": 10.2355335, + "maxlat": 56.2312300, + "maxlon": 10.2356728 + }, + "geometry": [ + { "lat": 56.2312155, "lon": 10.2355335 }, + { "lat": 56.2311810, "lon": 10.2356127 }, + { "lat": 56.2311951, "lon": 10.2356728 }, + { "lat": 56.2312300, "lon": 10.2355960 }, + { "lat": 56.2312155, "lon": 10.2355335 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1329105565, + "bounds": { + "minlat": 56.2373577, + "minlon": 10.2340207, + "maxlat": 56.2374132, + "maxlon": 10.2341027 + }, + "geometry": [ + { "lat": 56.2374132, "lon": 10.2340733 }, + { "lat": 56.2374026, "lon": 10.2340207 }, + { "lat": 56.2373577, "lon": 10.2340500 }, + { "lat": 56.2373683, "lon": 10.2341027 }, + { "lat": 56.2374132, "lon": 10.2340733 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1329105566, + "bounds": { + "minlat": 56.2373683, + "minlon": 10.2340733, + "maxlat": 56.2374235, + "maxlon": 10.2341529 + }, + "geometry": [ + { "lat": 56.2374132, "lon": 10.2340733 }, + { "lat": 56.2374235, "lon": 10.2341224 }, + { "lat": 56.2373781, "lon": 10.2341529 }, + { "lat": 56.2373683, "lon": 10.2341027 }, + { "lat": 56.2374132, "lon": 10.2340733 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1347046022, + "bounds": { + "minlat": 56.1898357, + "minlon": 10.1679326, + "maxlat": 56.1899614, + "maxlon": 10.1685399 + }, + "geometry": [ + { "lat": 56.1898784, "lon": 10.1679326 }, + { "lat": 56.1898357, "lon": 10.1679582 }, + { "lat": 56.1898694, "lon": 10.1681895 }, + { "lat": 56.1898777, "lon": 10.1682468 }, + { "lat": 56.1898861, "lon": 10.1683040 }, + { "lat": 56.1898940, "lon": 10.1683585 }, + { "lat": 56.1899020, "lon": 10.1684135 }, + { "lat": 56.1899092, "lon": 10.1684632 }, + { "lat": 56.1898796, "lon": 10.1684760 }, + { "lat": 56.1898861, "lon": 10.1685399 }, + { "lat": 56.1899614, "lon": 10.1685058 }, + { "lat": 56.1899524, "lon": 10.1684437 }, + { "lat": 56.1899450, "lon": 10.1683923 }, + { "lat": 56.1899368, "lon": 10.1683359 }, + { "lat": 56.1899290, "lon": 10.1682817 }, + { "lat": 56.1899207, "lon": 10.1682244 }, + { "lat": 56.1899125, "lon": 10.1681678 }, + { "lat": 56.1898784, "lon": 10.1679326 } + ], + "tags": { + "amenity": "parking", + "capacity": "12", + "capacity:disabled": "6", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1368819791, + "bounds": { + "minlat": 56.1525917, + "minlon": 10.1912279, + "maxlat": 56.1526486, + "maxlon": 10.1913302 + }, + "geometry": [ + { "lat": 56.1525917, "lon": 10.1912825 }, + { "lat": 56.1526156, "lon": 10.1912279 }, + { "lat": 56.1526486, "lon": 10.1912774 }, + { "lat": 56.1526256, "lon": 10.1913302 }, + { "lat": 56.1525917, "lon": 10.1912825 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1374371712, + "bounds": { + "minlat": 56.2757218, + "minlon": 10.3143167, + "maxlat": 56.2763078, + "maxlon": 10.3153431 + }, + "geometry": [ + { "lat": 56.2759680, "lon": 10.3151366 }, + { "lat": 56.2759307, "lon": 10.3151044 }, + { "lat": 56.2759413, "lon": 10.3150645 }, + { "lat": 56.2759215, "lon": 10.3150462 }, + { "lat": 56.2758944, "lon": 10.3150214 }, + { "lat": 56.2758558, "lon": 10.3149839 }, + { "lat": 56.2757218, "lon": 10.3148572 }, + { "lat": 56.2757825, "lon": 10.3146661 }, + { "lat": 56.2758536, "lon": 10.3146701 }, + { "lat": 56.2759098, "lon": 10.3143167 }, + { "lat": 56.2760033, "lon": 10.3143235 }, + { "lat": 56.2760626, "lon": 10.3143526 }, + { "lat": 56.2760254, "lon": 10.3145856 }, + { "lat": 56.2759856, "lon": 10.3145652 }, + { "lat": 56.2759389, "lon": 10.3148472 }, + { "lat": 56.2760135, "lon": 10.3148964 }, + { "lat": 56.2760237, "lon": 10.3148337 }, + { "lat": 56.2763078, "lon": 10.3149819 }, + { "lat": 56.2762819, "lon": 10.3152677 }, + { "lat": 56.2762615, "lon": 10.3153431 }, + { "lat": 56.2762169, "lon": 10.3153035 }, + { "lat": 56.2759772, "lon": 10.3151021 }, + { "lat": 56.2759680, "lon": 10.3151366 } + ], + "tags": { + "amenity": "parking", + "capacity:charging": "2", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1374690739, + "bounds": { + "minlat": 56.1387500, + "minlon": 10.2116843, + "maxlat": 56.1388243, + "maxlon": 10.2118382 + }, + "geometry": [ + { "lat": 56.1387968, "lon": 10.2118382 }, + { "lat": 56.1388243, "lon": 10.2117191 }, + { "lat": 56.1387775, "lon": 10.2116843 }, + { "lat": 56.1387500, "lon": 10.2118033 }, + { "lat": 56.1387968, "lon": 10.2118382 } + ], + "tags": { + "access": "yes", + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1377165736, + "bounds": { + "minlat": 56.1450044, + "minlon": 10.2056306, + "maxlat": 56.1450928, + "maxlon": 10.2057147 + }, + "geometry": [ + { "lat": 56.1450928, "lon": 10.2056361 }, + { "lat": 56.1450062, "lon": 10.2056306 }, + { "lat": 56.1450044, "lon": 10.2057084 }, + { "lat": 56.1450907, "lon": 10.2057147 }, + { "lat": 56.1450928, "lon": 10.2056361 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2" + } +}, +{ + "type": "way", + "id": 1383807183, + "bounds": { + "minlat": 56.1933734, + "minlon": 10.1646002, + "maxlat": 56.1934224, + "maxlon": 10.1647174 + }, + "geometry": [ + { "lat": 56.1933886, "lon": 10.1646157 }, + { "lat": 56.1933734, "lon": 10.1647174 }, + { "lat": 56.1934073, "lon": 10.1647031 }, + { "lat": 56.1934224, "lon": 10.1646002 }, + { "lat": 56.1933886, "lon": 10.1646157 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839011, + "bounds": { + "minlat": 56.1898796, + "minlon": 10.1684437, + "maxlat": 56.1899614, + "maxlon": 10.1685399 + }, + "geometry": [ + { "lat": 56.1899524, "lon": 10.1684437 }, + { "lat": 56.1899092, "lon": 10.1684632 }, + { "lat": 56.1898796, "lon": 10.1684760 }, + { "lat": 56.1898861, "lon": 10.1685399 }, + { "lat": 56.1899614, "lon": 10.1685058 }, + { "lat": 56.1899524, "lon": 10.1684437 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839013, + "bounds": { + "minlat": 56.1931907, + "minlon": 10.1701028, + "maxlat": 56.1932325, + "maxlon": 10.1701506 + }, + "geometry": [ + { "lat": 56.1932325, "lon": 10.1701053 }, + { "lat": 56.1932324, "lon": 10.1701506 }, + { "lat": 56.1931907, "lon": 10.1701496 }, + { "lat": 56.1931915, "lon": 10.1701028 }, + { "lat": 56.1932325, "lon": 10.1701053 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839014, + "bounds": { + "minlat": 56.1931904, + "minlon": 10.1701496, + "maxlat": 56.1932324, + "maxlon": 10.1701984 + }, + "geometry": [ + { "lat": 56.1932324, "lon": 10.1701506 }, + { "lat": 56.1932320, "lon": 10.1701984 }, + { "lat": 56.1931904, "lon": 10.1701974 }, + { "lat": 56.1931907, "lon": 10.1701496 }, + { "lat": 56.1932324, "lon": 10.1701506 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839015, + "bounds": { + "minlat": 56.1933148, + "minlon": 10.1706868, + "maxlat": 56.1933652, + "maxlon": 10.1707787 + }, + "geometry": [ + { "lat": 56.1933652, "lon": 10.1707218 }, + { "lat": 56.1933359, "lon": 10.1707787 }, + { "lat": 56.1933148, "lon": 10.1707436 }, + { "lat": 56.1933446, "lon": 10.1706868 }, + { "lat": 56.1933652, "lon": 10.1707218 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839016, + "bounds": { + "minlat": 56.1932962, + "minlon": 10.1706552, + "maxlat": 56.1933446, + "maxlon": 10.1707436 + }, + "geometry": [ + { "lat": 56.1933446, "lon": 10.1706868 }, + { "lat": 56.1933148, "lon": 10.1707436 }, + { "lat": 56.1932962, "lon": 10.1707120 }, + { "lat": 56.1933260, "lon": 10.1706552 }, + { "lat": 56.1933446, "lon": 10.1706868 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839017, + "bounds": { + "minlat": 56.1940719, + "minlon": 10.1703128, + "maxlat": 56.1941241, + "maxlon": 10.1703960 + }, + "geometry": [ + { "lat": 56.1940719, "lon": 10.1703400 }, + { "lat": 56.1940835, "lon": 10.1703960 }, + { "lat": 56.1941241, "lon": 10.1703687 }, + { "lat": 56.1941125, "lon": 10.1703128 }, + { "lat": 56.1940719, "lon": 10.1703400 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839018, + "bounds": { + "minlat": 56.1940835, + "minlon": 10.1703687, + "maxlat": 56.1941359, + "maxlon": 10.1704531 + }, + "geometry": [ + { "lat": 56.1940835, "lon": 10.1703960 }, + { "lat": 56.1940955, "lon": 10.1704531 }, + { "lat": 56.1941359, "lon": 10.1704255 }, + { "lat": 56.1941241, "lon": 10.1703687 }, + { "lat": 56.1940835, "lon": 10.1703960 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839019, + "bounds": { + "minlat": 56.1940955, + "minlon": 10.1704255, + "maxlat": 56.1941489, + "maxlon": 10.1705156 + }, + "geometry": [ + { "lat": 56.1940955, "lon": 10.1704531 }, + { "lat": 56.1941085, "lon": 10.1705156 }, + { "lat": 56.1941489, "lon": 10.1704900 }, + { "lat": 56.1941359, "lon": 10.1704255 }, + { "lat": 56.1940955, "lon": 10.1704531 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839022, + "bounds": { + "minlat": 56.1934509, + "minlon": 10.1688599, + "maxlat": 56.1935025, + "maxlon": 10.1689426 + }, + "geometry": [ + { "lat": 56.1935025, "lon": 10.1689164 }, + { "lat": 56.1934912, "lon": 10.1688599 }, + { "lat": 56.1934509, "lon": 10.1688861 }, + { "lat": 56.1934622, "lon": 10.1689426 }, + { "lat": 56.1935025, "lon": 10.1689164 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839023, + "bounds": { + "minlat": 56.1920864, + "minlon": 10.1660033, + "maxlat": 56.1921389, + "maxlon": 10.1660932 + }, + "geometry": [ + { "lat": 56.1921218, "lon": 10.1660033 }, + { "lat": 56.1920864, "lon": 10.1660411 }, + { "lat": 56.1921035, "lon": 10.1660932 }, + { "lat": 56.1921389, "lon": 10.1660564 }, + { "lat": 56.1921218, "lon": 10.1660033 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839024, + "bounds": { + "minlat": 56.1921035, + "minlon": 10.1660564, + "maxlat": 56.1921554, + "maxlon": 10.1661444 + }, + "geometry": [ + { "lat": 56.1921389, "lon": 10.1660564 }, + { "lat": 56.1921554, "lon": 10.1661075 }, + { "lat": 56.1921200, "lon": 10.1661444 }, + { "lat": 56.1921035, "lon": 10.1660932 }, + { "lat": 56.1921389, "lon": 10.1660564 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839025, + "bounds": { + "minlat": 56.1921200, + "minlon": 10.1661075, + "maxlat": 56.1921715, + "maxlon": 10.1661949 + }, + "geometry": [ + { "lat": 56.1921554, "lon": 10.1661075 }, + { "lat": 56.1921715, "lon": 10.1661582 }, + { "lat": 56.1921364, "lon": 10.1661949 }, + { "lat": 56.1921200, "lon": 10.1661444 }, + { "lat": 56.1921554, "lon": 10.1661075 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839026, + "bounds": { + "minlat": 56.1921364, + "minlon": 10.1661582, + "maxlat": 56.1921909, + "maxlon": 10.1662537 + }, + "geometry": [ + { "lat": 56.1921715, "lon": 10.1661582 }, + { "lat": 56.1921909, "lon": 10.1662166 }, + { "lat": 56.1921548, "lon": 10.1662537 }, + { "lat": 56.1921364, "lon": 10.1661949 }, + { "lat": 56.1921715, "lon": 10.1661582 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839027, + "bounds": { + "minlat": 56.1921805, + "minlon": 10.1662984, + "maxlat": 56.1922334, + "maxlon": 10.1663883 + }, + "geometry": [ + { "lat": 56.1922334, "lon": 10.1663509 }, + { "lat": 56.1921973, "lon": 10.1663883 }, + { "lat": 56.1921805, "lon": 10.1663357 }, + { "lat": 56.1922165, "lon": 10.1662984 }, + { "lat": 56.1922334, "lon": 10.1663509 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839028, + "bounds": { + "minlat": 56.1921660, + "minlon": 10.1662527, + "maxlat": 56.1922165, + "maxlon": 10.1663357 + }, + "geometry": [ + { "lat": 56.1922165, "lon": 10.1662984 }, + { "lat": 56.1922017, "lon": 10.1662527 }, + { "lat": 56.1921660, "lon": 10.1662910 }, + { "lat": 56.1921805, "lon": 10.1663357 }, + { "lat": 56.1922165, "lon": 10.1662984 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839029, + "bounds": { + "minlat": 56.1919356, + "minlon": 10.1723930, + "maxlat": 56.1920067, + "maxlon": 10.1724789 + }, + "geometry": [ + { "lat": 56.1919972, "lon": 10.1723930 }, + { "lat": 56.1920067, "lon": 10.1724373 }, + { "lat": 56.1919452, "lon": 10.1724789 }, + { "lat": 56.1919356, "lon": 10.1724346 }, + { "lat": 56.1919972, "lon": 10.1723930 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839030, + "bounds": { + "minlat": 56.1919452, + "minlon": 10.1724373, + "maxlat": 56.1920157, + "maxlon": 10.1725214 + }, + "geometry": [ + { "lat": 56.1920067, "lon": 10.1724373 }, + { "lat": 56.1920157, "lon": 10.1724795 }, + { "lat": 56.1919544, "lon": 10.1725214 }, + { "lat": 56.1919452, "lon": 10.1724789 }, + { "lat": 56.1920067, "lon": 10.1724373 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839031, + "bounds": { + "minlat": 56.1919544, + "minlon": 10.1724795, + "maxlat": 56.1920256, + "maxlon": 10.1725679 + }, + "geometry": [ + { "lat": 56.1920157, "lon": 10.1724795 }, + { "lat": 56.1920256, "lon": 10.1725256 }, + { "lat": 56.1919645, "lon": 10.1725679 }, + { "lat": 56.1919544, "lon": 10.1725214 }, + { "lat": 56.1920157, "lon": 10.1724795 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839032, + "bounds": { + "minlat": 56.1919645, + "minlon": 10.1725256, + "maxlat": 56.1920349, + "maxlon": 10.1726101 + }, + "geometry": [ + { "lat": 56.1920256, "lon": 10.1725256 }, + { "lat": 56.1920349, "lon": 10.1725688 }, + { "lat": 56.1919736, "lon": 10.1726101 }, + { "lat": 56.1919645, "lon": 10.1725679 }, + { "lat": 56.1920256, "lon": 10.1725256 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839033, + "bounds": { + "minlat": 56.1919736, + "minlon": 10.1725688, + "maxlat": 56.1920454, + "maxlon": 10.1726562 + }, + "geometry": [ + { "lat": 56.1920349, "lon": 10.1725688 }, + { "lat": 56.1920454, "lon": 10.1726180 }, + { "lat": 56.1919836, "lon": 10.1726562 }, + { "lat": 56.1919736, "lon": 10.1726101 }, + { "lat": 56.1920349, "lon": 10.1725688 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839035, + "bounds": { + "minlat": 56.1905160, + "minlon": 10.1749838, + "maxlat": 56.1905887, + "maxlon": 10.1750893 + }, + "geometry": [ + { "lat": 56.1905887, "lon": 10.1750464 }, + { "lat": 56.1905301, "lon": 10.1750893 }, + { "lat": 56.1905160, "lon": 10.1750231 }, + { "lat": 56.1905748, "lon": 10.1749838 }, + { "lat": 56.1905887, "lon": 10.1750464 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839036, + "bounds": { + "minlat": 56.1905026, + "minlon": 10.1749204, + "maxlat": 56.1905748, + "maxlon": 10.1750231 + }, + "geometry": [ + { "lat": 56.1905160, "lon": 10.1750231 }, + { "lat": 56.1905026, "lon": 10.1749612 }, + { "lat": 56.1905616, "lon": 10.1749204 }, + { "lat": 56.1905748, "lon": 10.1749838 }, + { "lat": 56.1905160, "lon": 10.1750231 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839037, + "bounds": { + "minlat": 56.1904905, + "minlon": 10.1748645, + "maxlat": 56.1905616, + "maxlon": 10.1749612 + }, + "geometry": [ + { "lat": 56.1905026, "lon": 10.1749612 }, + { "lat": 56.1904905, "lon": 10.1749040 }, + { "lat": 56.1905484, "lon": 10.1748645 }, + { "lat": 56.1905616, "lon": 10.1749204 }, + { "lat": 56.1905026, "lon": 10.1749612 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839038, + "bounds": { + "minlat": 56.1888816, + "minlon": 10.1747964, + "maxlat": 56.1889214, + "maxlon": 10.1748746 + }, + "geometry": [ + { "lat": 56.1889110, "lon": 10.1747964 }, + { "lat": 56.1888816, "lon": 10.1748032 }, + { "lat": 56.1888849, "lon": 10.1748746 }, + { "lat": 56.1889214, "lon": 10.1748639 }, + { "lat": 56.1889110, "lon": 10.1747964 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839039, + "bounds": { + "minlat": 56.1888441, + "minlon": 10.1748032, + "maxlat": 56.1888849, + "maxlon": 10.1748746 + }, + "geometry": [ + { "lat": 56.1888816, "lon": 10.1748032 }, + { "lat": 56.1888462, "lon": 10.1748052 }, + { "lat": 56.1888441, "lon": 10.1748717 }, + { "lat": 56.1888849, "lon": 10.1748746 }, + { "lat": 56.1888816, "lon": 10.1748032 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839040, + "bounds": { + "minlat": 56.1889045, + "minlon": 10.1749832, + "maxlat": 56.1889426, + "maxlon": 10.1750605 + }, + "geometry": [ + { "lat": 56.1889045, "lon": 10.1749891 }, + { "lat": 56.1889089, "lon": 10.1750605 }, + { "lat": 56.1889426, "lon": 10.1750546 }, + { "lat": 56.1889366, "lon": 10.1749832 }, + { "lat": 56.1889045, "lon": 10.1749891 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383839041, + "bounds": { + "minlat": 56.1888702, + "minlon": 10.1749891, + "maxlat": 56.1889089, + "maxlon": 10.1750644 + }, + "geometry": [ + { "lat": 56.1889089, "lon": 10.1750605 }, + { "lat": 56.1888740, "lon": 10.1750644 }, + { "lat": 56.1888702, "lon": 10.1749910 }, + { "lat": 56.1889045, "lon": 10.1749891 }, + { "lat": 56.1889089, "lon": 10.1750605 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383840748, + "bounds": { + "minlat": 56.1898694, + "minlon": 10.1681678, + "maxlat": 56.1899207, + "maxlon": 10.1682468 + }, + "geometry": [ + { "lat": 56.1899125, "lon": 10.1681678 }, + { "lat": 56.1898694, "lon": 10.1681895 }, + { "lat": 56.1898777, "lon": 10.1682468 }, + { "lat": 56.1899207, "lon": 10.1682244 }, + { "lat": 56.1899125, "lon": 10.1681678 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383840749, + "bounds": { + "minlat": 56.1898777, + "minlon": 10.1682244, + "maxlat": 56.1899290, + "maxlon": 10.1683040 + }, + "geometry": [ + { "lat": 56.1899207, "lon": 10.1682244 }, + { "lat": 56.1899290, "lon": 10.1682817 }, + { "lat": 56.1898861, "lon": 10.1683040 }, + { "lat": 56.1898777, "lon": 10.1682468 }, + { "lat": 56.1899207, "lon": 10.1682244 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383840750, + "bounds": { + "minlat": 56.1898861, + "minlon": 10.1682817, + "maxlat": 56.1899368, + "maxlon": 10.1683585 + }, + "geometry": [ + { "lat": 56.1899290, "lon": 10.1682817 }, + { "lat": 56.1899368, "lon": 10.1683359 }, + { "lat": 56.1898940, "lon": 10.1683585 }, + { "lat": 56.1898861, "lon": 10.1683040 }, + { "lat": 56.1899290, "lon": 10.1682817 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383840751, + "bounds": { + "minlat": 56.1898940, + "minlon": 10.1683359, + "maxlat": 56.1899450, + "maxlon": 10.1684135 + }, + "geometry": [ + { "lat": 56.1899368, "lon": 10.1683359 }, + { "lat": 56.1899450, "lon": 10.1683923 }, + { "lat": 56.1899020, "lon": 10.1684135 }, + { "lat": 56.1898940, "lon": 10.1683585 }, + { "lat": 56.1899368, "lon": 10.1683359 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383961104, + "bounds": { + "minlat": 56.1942406, + "minlon": 10.1942824, + "maxlat": 56.1942834, + "maxlon": 10.1943771 + }, + "geometry": [ + { "lat": 56.1942406, "lon": 10.1943043 }, + { "lat": 56.1942646, "lon": 10.1942824 }, + { "lat": 56.1942834, "lon": 10.1943568 }, + { "lat": 56.1942587, "lon": 10.1943771 }, + { "lat": 56.1942406, "lon": 10.1943043 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1383961105, + "bounds": { + "minlat": 56.1942646, + "minlon": 10.1942608, + "maxlat": 56.1943095, + "maxlon": 10.1943568 + }, + "geometry": [ + { "lat": 56.1942646, "lon": 10.1942824 }, + { "lat": 56.1942917, "lon": 10.1942608 }, + { "lat": 56.1943095, "lon": 10.1943355 }, + { "lat": 56.1942834, "lon": 10.1943568 }, + { "lat": 56.1942646, "lon": 10.1942824 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1384127785, + "bounds": { + "minlat": 56.1586105, + "minlon": 10.1994204, + "maxlat": 56.1586506, + "maxlon": 10.1994671 + }, + "geometry": [ + { "lat": 56.1586147, "lon": 10.1994204 }, + { "lat": 56.1586105, "lon": 10.1994520 }, + { "lat": 56.1586456, "lon": 10.1994671 }, + { "lat": 56.1586506, "lon": 10.1994343 }, + { "lat": 56.1586147, "lon": 10.1994204 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1384127786, + "bounds": { + "minlat": 56.1588400, + "minlon": 10.1994097, + "maxlat": 56.1589163, + "maxlon": 10.1994608 + }, + "geometry": [ + { "lat": 56.1588400, "lon": 10.1994488 }, + { "lat": 56.1588706, "lon": 10.1994608 }, + { "lat": 56.1589163, "lon": 10.1994236 }, + { "lat": 56.1588861, "lon": 10.1994097 }, + { "lat": 56.1588400, "lon": 10.1994488 } + ], + "tags": { + "access": "permit", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1384128171, + "bounds": { + "minlat": 56.1943113, + "minlon": 10.1944455, + "maxlat": 56.1943536, + "maxlon": 10.1945255 + }, + "geometry": [ + { "lat": 56.1943396, "lon": 10.1944455 }, + { "lat": 56.1943536, "lon": 10.1945034 }, + { "lat": 56.1943253, "lon": 10.1945255 }, + { "lat": 56.1943113, "lon": 10.1944676 }, + { "lat": 56.1943396, "lon": 10.1944455 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1384139202, + "bounds": { + "minlat": 56.1877179, + "minlon": 10.1701647, + "maxlat": 56.1877910, + "maxlon": 10.1703136 + }, + "geometry": [ + { "lat": 56.1877910, "lon": 10.1701647 }, + { "lat": 56.1877589, "lon": 10.1702866 }, + { "lat": 56.1877179, "lon": 10.1703136 }, + { "lat": 56.1877498, "lon": 10.1701941 }, + { "lat": 56.1877910, "lon": 10.1701647 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1384139203, + "bounds": { + "minlat": 56.1876742, + "minlon": 10.1701941, + "maxlat": 56.1877498, + "maxlon": 10.1703425 + }, + "geometry": [ + { "lat": 56.1877179, "lon": 10.1703136 }, + { "lat": 56.1876742, "lon": 10.1703425 }, + { "lat": 56.1877086, "lon": 10.1702236 }, + { "lat": 56.1877498, "lon": 10.1701941 }, + { "lat": 56.1877179, "lon": 10.1703136 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1388077760, + "bounds": { + "minlat": 56.1389781, + "minlon": 10.2118885, + "maxlat": 56.1390839, + "maxlon": 10.2121716 + }, + "geometry": [ + { "lat": 56.1390253, "lon": 10.2121716 }, + { "lat": 56.1389781, "lon": 10.2121399 }, + { "lat": 56.1390383, "lon": 10.2118885 }, + { "lat": 56.1390839, "lon": 10.2119252 }, + { "lat": 56.1390253, "lon": 10.2121716 } + ], + "tags": { + "access": "yes", + "amenity": "parking_space", + "capacity": "5", + "parking_space": "disabled", + "surface": "grass_paver" + } +}, +{ + "type": "way", + "id": 1395039553, + "bounds": { + "minlat": 56.2251747, + "minlon": 10.2380858, + "maxlat": 56.2252381, + "maxlon": 10.2381646 + }, + "geometry": [ + { "lat": 56.2252381, "lon": 10.2381015 }, + { "lat": 56.2251800, "lon": 10.2380858 }, + { "lat": 56.2251747, "lon": 10.2381479 }, + { "lat": 56.2252328, "lon": 10.2381646 }, + { "lat": 56.2252381, "lon": 10.2381015 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1395039554, + "bounds": { + "minlat": 56.2251704, + "minlon": 10.2381479, + "maxlat": 56.2252328, + "maxlon": 10.2382215 + }, + "geometry": [ + { "lat": 56.2252328, "lon": 10.2381646 }, + { "lat": 56.2252281, "lon": 10.2382215 }, + { "lat": 56.2251704, "lon": 10.2382054 }, + { "lat": 56.2251747, "lon": 10.2381479 }, + { "lat": 56.2252328, "lon": 10.2381646 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1395040245, + "bounds": { + "minlat": 56.2248497, + "minlon": 10.2374550, + "maxlat": 56.2249052, + "maxlon": 10.2375327 + }, + "geometry": [ + { "lat": 56.2249052, "lon": 10.2374701 }, + { "lat": 56.2248990, "lon": 10.2375327 }, + { "lat": 56.2248497, "lon": 10.2375176 }, + { "lat": 56.2248552, "lon": 10.2374550 }, + { "lat": 56.2249052, "lon": 10.2374701 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1395040246, + "bounds": { + "minlat": 56.2248447, + "minlon": 10.2375176, + "maxlat": 56.2248990, + "maxlon": 10.2375849 + }, + "geometry": [ + { "lat": 56.2248990, "lon": 10.2375327 }, + { "lat": 56.2248941, "lon": 10.2375849 }, + { "lat": 56.2248447, "lon": 10.2375698 }, + { "lat": 56.2248497, "lon": 10.2375176 }, + { "lat": 56.2248990, "lon": 10.2375327 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1410177010, + "bounds": { + "minlat": 56.1522957, + "minlon": 10.1994052, + "maxlat": 56.1524176, + "maxlon": 10.1996474 + }, + "geometry": [ + { "lat": 56.1524176, "lon": 10.1994716 }, + { "lat": 56.1523604, "lon": 10.1994052 }, + { "lat": 56.1522957, "lon": 10.1995855 }, + { "lat": 56.1523544, "lon": 10.1996474 }, + { "lat": 56.1524176, "lon": 10.1994716 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "1", + "fee": "no", + "maxstay": "15 minutes", + "parking": "surface", + "surface": "paving_stones" + } +}, +{ + "type": "way", + "id": 1436931350, + "bounds": { + "minlat": 56.2027690, + "minlon": 10.2346632, + "maxlat": 56.2028173, + "maxlon": 10.2347524 + }, + "geometry": [ + { "lat": 56.2027690, "lon": 10.2347401 }, + { "lat": 56.2028099, "lon": 10.2347524 }, + { "lat": 56.2028173, "lon": 10.2346755 }, + { "lat": 56.2027760, "lon": 10.2346632 }, + { "lat": 56.2027690, "lon": 10.2347401 } + ], + "tags": { + "access": "unknown", + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1441606719, + "bounds": { + "minlat": 56.2034108, + "minlon": 10.2332037, + "maxlat": 56.2035526, + "maxlon": 10.2333203 + }, + "geometry": [ + { "lat": 56.2034108, "lon": 10.2332821 }, + { "lat": 56.2034177, "lon": 10.2332037 }, + { "lat": 56.2035526, "lon": 10.2332419 }, + { "lat": 56.2035457, "lon": 10.2333203 }, + { "lat": 56.2034108, "lon": 10.2332821 } + ], + "tags": { + "access": "permit", + "amenity": "parking", + "capacity:disabled": "6", + "orientation": "perpendicular", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1441606720, + "bounds": { + "minlat": 56.2035666, + "minlon": 10.2332474, + "maxlat": 56.2037076, + "maxlon": 10.2333628 + }, + "geometry": [ + { "lat": 56.2035666, "lon": 10.2333248 }, + { "lat": 56.2035733, "lon": 10.2332474 }, + { "lat": 56.2037076, "lon": 10.2332854 }, + { "lat": 56.2037008, "lon": 10.2333628 }, + { "lat": 56.2035666, "lon": 10.2333248 } + ], + "tags": { + "access": "permit", + "amenity": "parking", + "capacity:disabled": "6", + "orientation": "perpendicular", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1441606724, + "bounds": { + "minlat": 56.2037471, + "minlon": 10.2338572, + "maxlat": 56.2038241, + "maxlon": 10.2339491 + }, + "geometry": [ + { "lat": 56.2037524, "lon": 10.2339491 }, + { "lat": 56.2037797, "lon": 10.2339430 }, + { "lat": 56.2038241, "lon": 10.2339331 }, + { "lat": 56.2038189, "lon": 10.2338572 }, + { "lat": 56.2037471, "lon": 10.2338732 }, + { "lat": 56.2037524, "lon": 10.2339491 } + ], + "tags": { + "access": "permit", + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1441606730, + "bounds": { + "minlat": 56.2039420, + "minlon": 10.2346238, + "maxlat": 56.2040185, + "maxlon": 10.2347232 + }, + "geometry": [ + { "lat": 56.2039420, "lon": 10.2346409 }, + { "lat": 56.2039480, "lon": 10.2347232 }, + { "lat": 56.2039918, "lon": 10.2347130 }, + { "lat": 56.2040185, "lon": 10.2347065 }, + { "lat": 56.2040123, "lon": 10.2346238 }, + { "lat": 56.2039420, "lon": 10.2346409 } + ], + "tags": { + "access": "permit", + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1448374382, + "bounds": { + "minlat": 56.1867287, + "minlon": 10.2284430, + "maxlat": 56.1867918, + "maxlon": 10.2285799 + }, + "geometry": [ + { "lat": 56.1867918, "lon": 10.2284631 }, + { "lat": 56.1867396, "lon": 10.2284430 }, + { "lat": 56.1867287, "lon": 10.2285601 }, + { "lat": 56.1867821, "lon": 10.2285799 }, + { "lat": 56.1867918, "lon": 10.2284631 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1451025164, + "bounds": { + "minlat": 56.1648564, + "minlon": 10.2054590, + "maxlat": 56.1649260, + "maxlon": 10.2056354 + }, + "geometry": [ + { "lat": 56.1648957, "lon": 10.2054590 }, + { "lat": 56.1649260, "lon": 10.2056100 }, + { "lat": 56.1648866, "lon": 10.2056354 }, + { "lat": 56.1648564, "lon": 10.2054844 }, + { "lat": 56.1648957, "lon": 10.2054590 } + ], + "tags": { + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "4", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1460294668, + "bounds": { + "minlat": 56.2157373, + "minlon": 10.1480403, + "maxlat": 56.2159185, + "maxlon": 10.1484208 + }, + "geometry": [ + { "lat": 56.2158710, "lon": 10.1480403 }, + { "lat": 56.2158495, "lon": 10.1480909 }, + { "lat": 56.2158360, "lon": 10.1481227 }, + { "lat": 56.2158165, "lon": 10.1481688 }, + { "lat": 56.2157980, "lon": 10.1482125 }, + { "lat": 56.2157790, "lon": 10.1482573 }, + { "lat": 56.2157600, "lon": 10.1483020 }, + { "lat": 56.2157373, "lon": 10.1483557 }, + { "lat": 56.2157849, "lon": 10.1484208 }, + { "lat": 56.2158085, "lon": 10.1483651 }, + { "lat": 56.2158274, "lon": 10.1483204 }, + { "lat": 56.2158467, "lon": 10.1482749 }, + { "lat": 56.2158653, "lon": 10.1482311 }, + { "lat": 56.2158833, "lon": 10.1481885 }, + { "lat": 56.2158969, "lon": 10.1481564 }, + { "lat": 56.2159185, "lon": 10.1481054 }, + { "lat": 56.2158710, "lon": 10.1480403 } + ], + "tags": { + "amenity": "parking", + "capacity": "6", + "capacity:disabled": "6", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1461492996, + "bounds": { + "minlat": 56.0464362, + "minlon": 10.1909576, + "maxlat": 56.0464877, + "maxlon": 10.1912190 + }, + "geometry": [ + { "lat": 56.0464643, "lon": 10.1909576 }, + { "lat": 56.0464877, "lon": 10.1909649 }, + { "lat": 56.0464595, "lon": 10.1912190 }, + { "lat": 56.0464362, "lon": 10.1912117 }, + { "lat": 56.0464643, "lon": 10.1909576 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1466175463, + "bounds": { + "minlat": 56.2330080, + "minlon": 10.2949029, + "maxlat": 56.2331005, + "maxlon": 10.2953628 + }, + "geometry": [ + { "lat": 56.2330539, "lon": 10.2953628 }, + { "lat": 56.2330080, "lon": 10.2953428 }, + { "lat": 56.2330528, "lon": 10.2949029 }, + { "lat": 56.2331005, "lon": 10.2949212 }, + { "lat": 56.2330539, "lon": 10.2953628 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "7", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491396585, + "bounds": { + "minlat": 56.2158495, + "minlon": 10.1480403, + "maxlat": 56.2159185, + "maxlon": 10.1481564 + }, + "geometry": [ + { "lat": 56.2158495, "lon": 10.1480909 }, + { "lat": 56.2158969, "lon": 10.1481564 }, + { "lat": 56.2159185, "lon": 10.1481054 }, + { "lat": 56.2158710, "lon": 10.1480403 }, + { "lat": 56.2158495, "lon": 10.1480909 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491396586, + "bounds": { + "minlat": 56.2158165, + "minlon": 10.1481227, + "maxlat": 56.2158833, + "maxlon": 10.1482311 + }, + "geometry": [ + { "lat": 56.2158360, "lon": 10.1481227 }, + { "lat": 56.2158833, "lon": 10.1481885 }, + { "lat": 56.2158653, "lon": 10.1482311 }, + { "lat": 56.2158165, "lon": 10.1481688 }, + { "lat": 56.2158360, "lon": 10.1481227 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491396587, + "bounds": { + "minlat": 56.2157980, + "minlon": 10.1481688, + "maxlat": 56.2158653, + "maxlon": 10.1482749 + }, + "geometry": [ + { "lat": 56.2158653, "lon": 10.1482311 }, + { "lat": 56.2158467, "lon": 10.1482749 }, + { "lat": 56.2157980, "lon": 10.1482125 }, + { "lat": 56.2158165, "lon": 10.1481688 }, + { "lat": 56.2158653, "lon": 10.1482311 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491396588, + "bounds": { + "minlat": 56.2157790, + "minlon": 10.1482125, + "maxlat": 56.2158467, + "maxlon": 10.1483204 + }, + "geometry": [ + { "lat": 56.2158467, "lon": 10.1482749 }, + { "lat": 56.2158274, "lon": 10.1483204 }, + { "lat": 56.2157790, "lon": 10.1482573 }, + { "lat": 56.2157980, "lon": 10.1482125 }, + { "lat": 56.2158467, "lon": 10.1482749 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491396589, + "bounds": { + "minlat": 56.2157600, + "minlon": 10.1482573, + "maxlat": 56.2158274, + "maxlon": 10.1483651 + }, + "geometry": [ + { "lat": 56.2158274, "lon": 10.1483204 }, + { "lat": 56.2158085, "lon": 10.1483651 }, + { "lat": 56.2157600, "lon": 10.1483020 }, + { "lat": 56.2157790, "lon": 10.1482573 }, + { "lat": 56.2158274, "lon": 10.1483204 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491396590, + "bounds": { + "minlat": 56.2157373, + "minlon": 10.1483020, + "maxlat": 56.2158085, + "maxlon": 10.1484208 + }, + "geometry": [ + { "lat": 56.2158085, "lon": 10.1483651 }, + { "lat": 56.2157849, "lon": 10.1484208 }, + { "lat": 56.2157373, "lon": 10.1483557 }, + { "lat": 56.2157600, "lon": 10.1483020 }, + { "lat": 56.2158085, "lon": 10.1483651 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491414516, + "bounds": { + "minlat": 56.1592042, + "minlon": 10.2152008, + "maxlat": 56.1593052, + "maxlon": 10.2153660 + }, + "geometry": [ + { "lat": 56.1592254, "lon": 10.2153660 }, + { "lat": 56.1593052, "lon": 10.2152472 }, + { "lat": 56.1592849, "lon": 10.2152008 }, + { "lat": 56.1592042, "lon": 10.2153196 }, + { "lat": 56.1592254, "lon": 10.2153660 } + ], + "tags": { + "amenity": "parking", + "capacity": "3", + "capacity:disabled": "3", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491452027, + "bounds": { + "minlat": 56.0464820, + "minlon": 10.1915097, + "maxlat": 56.0465314, + "maxlon": 10.1915780 + }, + "geometry": [ + { "lat": 56.0464874, "lon": 10.1915097 }, + { "lat": 56.0465314, "lon": 10.1915231 }, + { "lat": 56.0465259, "lon": 10.1915780 }, + { "lat": 56.0464820, "lon": 10.1915632 }, + { "lat": 56.0464874, "lon": 10.1915097 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491452308, + "bounds": { + "minlat": 56.0464011, + "minlon": 10.1928204, + "maxlat": 56.0464349, + "maxlon": 10.1929034 + }, + "geometry": [ + { "lat": 56.0464011, "lon": 10.1928999 }, + { "lat": 56.0464039, "lon": 10.1928204 }, + { "lat": 56.0464349, "lon": 10.1928239 }, + { "lat": 56.0464321, "lon": 10.1929034 }, + { "lat": 56.0464011, "lon": 10.1928999 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1491453512, + "bounds": { + "minlat": 56.1452102, + "minlon": 10.1864693, + "maxlat": 56.1452552, + "maxlon": 10.1865503 + }, + "geometry": [ + { "lat": 56.1452552, "lon": 10.1864981 }, + { "lat": 56.1452265, "lon": 10.1865503 }, + { "lat": 56.1452102, "lon": 10.1865215 }, + { "lat": 56.1452389, "lon": 10.1864693 }, + { "lat": 56.1452552, "lon": 10.1864981 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491596163, + "bounds": { + "minlat": 56.1553051, + "minlon": 10.1957967, + "maxlat": 56.1553576, + "maxlon": 10.1958807 + }, + "geometry": [ + { "lat": 56.1553187, "lon": 10.1957967 }, + { "lat": 56.1553051, "lon": 10.1958311 }, + { "lat": 56.1553439, "lon": 10.1958807 }, + { "lat": 56.1553576, "lon": 10.1958463 }, + { "lat": 56.1553187, "lon": 10.1957967 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491599502, + "bounds": { + "minlat": 56.1597305, + "minlon": 10.1998091, + "maxlat": 56.1597717, + "maxlon": 10.1998741 + }, + "geometry": [ + { "lat": 56.1597305, "lon": 10.1998459 }, + { "lat": 56.1597410, "lon": 10.1998741 }, + { "lat": 56.1597717, "lon": 10.1998374 }, + { "lat": 56.1597612, "lon": 10.1998091 }, + { "lat": 56.1597305, "lon": 10.1998459 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491604094, + "bounds": { + "minlat": 56.1599322, + "minlon": 10.1999227, + "maxlat": 56.1599880, + "maxlon": 10.1999737 + }, + "geometry": [ + { "lat": 56.1599880, "lon": 10.1999320 }, + { "lat": 56.1599638, "lon": 10.1999227 }, + { "lat": 56.1599322, "lon": 10.1999631 }, + { "lat": 56.1599545, "lon": 10.1999737 }, + { "lat": 56.1599880, "lon": 10.1999320 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491604455, + "bounds": { + "minlat": 56.1599155, + "minlon": 10.1985551, + "maxlat": 56.1599656, + "maxlon": 10.1986424 + }, + "geometry": [ + { "lat": 56.1599155, "lon": 10.1985551 }, + { "lat": 56.1599229, "lon": 10.1986045 }, + { "lat": 56.1599656, "lon": 10.1986424 }, + { "lat": 56.1599572, "lon": 10.1985936 }, + { "lat": 56.1599155, "lon": 10.1985551 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491606396, + "bounds": { + "minlat": 56.1294140, + "minlon": 10.2111430, + "maxlat": 56.1294917, + "maxlon": 10.2112837 + }, + "geometry": [ + { "lat": 56.1294696, "lon": 10.2111781 }, + { "lat": 56.1294917, "lon": 10.2112295 }, + { "lat": 56.1294568, "lon": 10.2112837 }, + { "lat": 56.1294352, "lon": 10.2112520 }, + { "lat": 56.1294140, "lon": 10.2112209 }, + { "lat": 56.1294392, "lon": 10.2111430 }, + { "lat": 56.1294696, "lon": 10.2111781 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491606397, + "bounds": { + "minlat": 56.1294140, + "minlon": 10.2111430, + "maxlat": 56.1294696, + "maxlon": 10.2112520 + }, + "geometry": [ + { "lat": 56.1294140, "lon": 10.2112209 }, + { "lat": 56.1294392, "lon": 10.2111430 }, + { "lat": 56.1294696, "lon": 10.2111781 }, + { "lat": 56.1294352, "lon": 10.2112520 }, + { "lat": 56.1294140, "lon": 10.2112209 } + ], + "tags": { + "amenity": "parking_space", + "fee": "no", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491606398, + "bounds": { + "minlat": 56.1294352, + "minlon": 10.2111781, + "maxlat": 56.1294917, + "maxlon": 10.2112837 + }, + "geometry": [ + { "lat": 56.1294352, "lon": 10.2112520 }, + { "lat": 56.1294568, "lon": 10.2112837 }, + { "lat": 56.1294917, "lon": 10.2112295 }, + { "lat": 56.1294696, "lon": 10.2111781 }, + { "lat": 56.1294352, "lon": 10.2112520 } + ], + "tags": { + "amenity": "parking_space", + "fee": "no", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1491637922, + "bounds": { + "minlat": 56.1578876, + "minlon": 10.1949108, + "maxlat": 56.1579499, + "maxlon": 10.1949748 + }, + "geometry": [ + { "lat": 56.1578876, "lon": 10.1949447 }, + { "lat": 56.1578932, "lon": 10.1949748 }, + { "lat": 56.1579499, "lon": 10.1949408 }, + { "lat": 56.1579444, "lon": 10.1949108 }, + { "lat": 56.1578876, "lon": 10.1949447 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1491642228, + "bounds": { + "minlat": 56.1604379, + "minlon": 10.2000826, + "maxlat": 56.1604880, + "maxlon": 10.2001595 + }, + "geometry": [ + { "lat": 56.1604379, "lon": 10.2001292 }, + { "lat": 56.1604492, "lon": 10.2001595 }, + { "lat": 56.1604880, "lon": 10.2001129 }, + { "lat": 56.1604767, "lon": 10.2000826 }, + { "lat": 56.1604379, "lon": 10.2001292 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side", + "wheelchair": "yes" + } +}, +{ + "type": "way", + "id": 1493115632, + "bounds": { + "minlat": 56.1611948, + "minlon": 10.2049041, + "maxlat": 56.1612266, + "maxlon": 10.2049993 + }, + "geometry": [ + { "lat": 56.1612266, "lon": 10.2049906 }, + { "lat": 56.1612089, "lon": 10.2049993 }, + { "lat": 56.1611948, "lon": 10.2049131 }, + { "lat": 56.1612123, "lon": 10.2049041 }, + { "lat": 56.1612266, "lon": 10.2049906 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493115633, + "bounds": { + "minlat": 56.1612089, + "minlon": 10.2049906, + "maxlat": 56.1612390, + "maxlon": 10.2050754 + }, + "geometry": [ + { "lat": 56.1612266, "lon": 10.2049906 }, + { "lat": 56.1612390, "lon": 10.2050661 }, + { "lat": 56.1612214, "lon": 10.2050754 }, + { "lat": 56.1612089, "lon": 10.2049993 }, + { "lat": 56.1612266, "lon": 10.2049906 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493115634, + "bounds": { + "minlat": 56.1612214, + "minlon": 10.2050661, + "maxlat": 56.1612551, + "maxlon": 10.2051726 + }, + "geometry": [ + { "lat": 56.1612390, "lon": 10.2050661 }, + { "lat": 56.1612551, "lon": 10.2051636 }, + { "lat": 56.1612373, "lon": 10.2051726 }, + { "lat": 56.1612214, "lon": 10.2050754 }, + { "lat": 56.1612390, "lon": 10.2050661 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493115635, + "bounds": { + "minlat": 56.1612850, + "minlon": 10.2054917, + "maxlat": 56.1613247, + "maxlon": 10.2056143 + }, + "geometry": [ + { "lat": 56.1613067, "lon": 10.2054917 }, + { "lat": 56.1612850, "lon": 10.2055031 }, + { "lat": 56.1613030, "lon": 10.2056143 }, + { "lat": 56.1613247, "lon": 10.2056029 }, + { "lat": 56.1613067, "lon": 10.2054917 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493115636, + "bounds": { + "minlat": 56.1628815, + "minlon": 10.2094909, + "maxlat": 56.1629215, + "maxlon": 10.2095767 + }, + "geometry": [ + { "lat": 56.1629215, "lon": 10.2095080 }, + { "lat": 56.1629074, "lon": 10.2094909 }, + { "lat": 56.1628815, "lon": 10.2095596 }, + { "lat": 56.1628955, "lon": 10.2095767 }, + { "lat": 56.1629215, "lon": 10.2095080 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated" + } +}, +{ + "type": "way", + "id": 1493120906, + "bounds": { + "minlat": 56.1642761, + "minlon": 10.2179322, + "maxlat": 56.1643395, + "maxlon": 10.2180788 + }, + "geometry": [ + { "lat": 56.1642761, "lon": 10.2180543 }, + { "lat": 56.1643106, "lon": 10.2179322 }, + { "lat": 56.1643395, "lon": 10.2179573 }, + { "lat": 56.1643054, "lon": 10.2180788 }, + { "lat": 56.1642761, "lon": 10.2180543 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493120907, + "bounds": { + "minlat": 56.1651003, + "minlon": 10.2186760, + "maxlat": 56.1651623, + "maxlon": 10.2188056 + }, + "geometry": [ + { "lat": 56.1651371, "lon": 10.2188056 }, + { "lat": 56.1651623, "lon": 10.2187093 }, + { "lat": 56.1651249, "lon": 10.2186760 }, + { "lat": 56.1651003, "lon": 10.2187730 }, + { "lat": 56.1651371, "lon": 10.2188056 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493120908, + "bounds": { + "minlat": 56.1734101, + "minlon": 10.2186863, + "maxlat": 56.1734447, + "maxlon": 10.2188107 + }, + "geometry": [ + { "lat": 56.1734447, "lon": 10.2186945 }, + { "lat": 56.1734293, "lon": 10.2188107 }, + { "lat": 56.1734101, "lon": 10.2188026 }, + { "lat": 56.1734255, "lon": 10.2186863 }, + { "lat": 56.1734447, "lon": 10.2186945 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120909, + "bounds": { + "minlat": 56.1733826, + "minlon": 10.2189378, + "maxlat": 56.1734116, + "maxlon": 10.2190388 + }, + "geometry": [ + { "lat": 56.1734116, "lon": 10.2189449 }, + { "lat": 56.1733990, "lon": 10.2190388 }, + { "lat": 56.1733826, "lon": 10.2190317 }, + { "lat": 56.1733953, "lon": 10.2189378 }, + { "lat": 56.1734116, "lon": 10.2189449 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120910, + "bounds": { + "minlat": 56.1743782, + "minlon": 10.2178945, + "maxlat": 56.1744253, + "maxlon": 10.2181069 + }, + "geometry": [ + { "lat": 56.1744253, "lon": 10.2179029 }, + { "lat": 56.1744125, "lon": 10.2179957 }, + { "lat": 56.1743971, "lon": 10.2181069 }, + { "lat": 56.1743782, "lon": 10.2180985 }, + { "lat": 56.1743937, "lon": 10.2179861 }, + { "lat": 56.1744063, "lon": 10.2178945 }, + { "lat": 56.1744253, "lon": 10.2179029 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120911, + "bounds": { + "minlat": 56.1743455, + "minlon": 10.2182483, + "maxlat": 56.1743800, + "maxlon": 10.2183727 + }, + "geometry": [ + { "lat": 56.1743800, "lon": 10.2182565 }, + { "lat": 56.1743647, "lon": 10.2183727 }, + { "lat": 56.1743455, "lon": 10.2183646 }, + { "lat": 56.1743608, "lon": 10.2182483 }, + { "lat": 56.1743800, "lon": 10.2182565 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120912, + "bounds": { + "minlat": 56.1724363, + "minlon": 10.2143406, + "maxlat": 56.1725046, + "maxlon": 10.2143885 + }, + "geometry": [ + { "lat": 56.1725046, "lon": 10.2143758 }, + { "lat": 56.1724384, "lon": 10.2143885 }, + { "lat": 56.1724363, "lon": 10.2143534 }, + { "lat": 56.1725025, "lon": 10.2143406 }, + { "lat": 56.1725046, "lon": 10.2143758 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120913, + "bounds": { + "minlat": 56.1719911, + "minlon": 10.2136260, + "maxlat": 56.1720209, + "maxlon": 10.2137289 + }, + "geometry": [ + { "lat": 56.1720209, "lon": 10.2136330 }, + { "lat": 56.1720096, "lon": 10.2137289 }, + { "lat": 56.1719911, "lon": 10.2137218 }, + { "lat": 56.1720024, "lon": 10.2136260 }, + { "lat": 56.1720209, "lon": 10.2136330 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120914, + "bounds": { + "minlat": 56.1673982, + "minlon": 10.2057807, + "maxlat": 56.1674472, + "maxlon": 10.2058429 + }, + "geometry": [ + { "lat": 56.1674472, "lon": 10.2058123 }, + { "lat": 56.1674053, "lon": 10.2058429 }, + { "lat": 56.1673982, "lon": 10.2058113 }, + { "lat": 56.1674401, "lon": 10.2057807 }, + { "lat": 56.1674472, "lon": 10.2058123 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120915, + "bounds": { + "minlat": 56.1744450, + "minlon": 10.2017645, + "maxlat": 56.1744786, + "maxlon": 10.2018669 + }, + "geometry": [ + { "lat": 56.1744786, "lon": 10.2017743 }, + { "lat": 56.1744626, "lon": 10.2018669 }, + { "lat": 56.1744450, "lon": 10.2018571 }, + { "lat": 56.1744609, "lon": 10.2017645 }, + { "lat": 56.1744786, "lon": 10.2017743 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120916, + "bounds": { + "minlat": 56.1754831, + "minlon": 10.1956599, + "maxlat": 56.1755186, + "maxlon": 10.1957448 + }, + "geometry": [ + { "lat": 56.1755186, "lon": 10.1956727 }, + { "lat": 56.1755062, "lon": 10.1957448 }, + { "lat": 56.1754831, "lon": 10.1957320 }, + { "lat": 56.1754955, "lon": 10.1956599 }, + { "lat": 56.1755186, "lon": 10.1956727 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120917, + "bounds": { + "minlat": 56.1484989, + "minlon": 10.2057704, + "maxlat": 56.1485197, + "maxlon": 10.2058602 + }, + "geometry": [ + { "lat": 56.1485197, "lon": 10.2057718 }, + { "lat": 56.1485177, "lon": 10.2058602 }, + { "lat": 56.1484989, "lon": 10.2058589 }, + { "lat": 56.1485009, "lon": 10.2057704 }, + { "lat": 56.1485197, "lon": 10.2057718 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493120918, + "bounds": { + "minlat": 56.1478335, + "minlon": 10.1998834, + "maxlat": 56.1478803, + "maxlon": 10.1999141 + }, + "geometry": [ + { "lat": 56.1478801, "lon": 10.1999141 }, + { "lat": 56.1478335, "lon": 10.1999128 }, + { "lat": 56.1478337, "lon": 10.1998834 }, + { "lat": 56.1478803, "lon": 10.1998846 }, + { "lat": 56.1478801, "lon": 10.1999141 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493357507, + "bounds": { + "minlat": 56.1424514, + "minlon": 10.2056784, + "maxlat": 56.1425188, + "maxlon": 10.2057270 + }, + "geometry": [ + { "lat": 56.1425164, "lon": 10.2056784 }, + { "lat": 56.1424514, "lon": 10.2056988 }, + { "lat": 56.1424533, "lon": 10.2057270 }, + { "lat": 56.1425188, "lon": 10.2057101 }, + { "lat": 56.1425164, "lon": 10.2056784 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493357960, + "bounds": { + "minlat": 56.1418138, + "minlon": 10.2061722, + "maxlat": 56.1418488, + "maxlon": 10.2062590 + }, + "geometry": [ + { "lat": 56.1418340, "lon": 10.2061722 }, + { "lat": 56.1418138, "lon": 10.2061879 }, + { "lat": 56.1418288, "lon": 10.2062590 }, + { "lat": 56.1418488, "lon": 10.2062455 }, + { "lat": 56.1418340, "lon": 10.2061722 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "disabled": "designated", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493358224, + "bounds": { + "minlat": 56.1412975, + "minlon": 10.2069056, + "maxlat": 56.1413187, + "maxlon": 10.2070080 + }, + "geometry": [ + { "lat": 56.1412975, "lon": 10.2070058 }, + { "lat": 56.1413147, "lon": 10.2070080 }, + { "lat": 56.1413187, "lon": 10.2069078 }, + { "lat": 56.1413015, "lon": 10.2069056 }, + { "lat": 56.1412975, "lon": 10.2070058 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493358461, + "bounds": { + "minlat": 56.1434628, + "minlon": 10.2064465, + "maxlat": 56.1435157, + "maxlon": 10.2065244 + }, + "geometry": [ + { "lat": 56.1434737, "lon": 10.2064465 }, + { "lat": 56.1434628, "lon": 10.2064782 }, + { "lat": 56.1435049, "lon": 10.2065244 }, + { "lat": 56.1435157, "lon": 10.2064927 }, + { "lat": 56.1434737, "lon": 10.2064465 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493358784, + "bounds": { + "minlat": 56.1440417, + "minlon": 10.2068291, + "maxlat": 56.1440762, + "maxlon": 10.2068763 + }, + "geometry": [ + { "lat": 56.1440468, "lon": 10.2068291 }, + { "lat": 56.1440417, "lon": 10.2068616 }, + { "lat": 56.1440711, "lon": 10.2068763 }, + { "lat": 56.1440762, "lon": 10.2068438 }, + { "lat": 56.1440468, "lon": 10.2068291 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493360446, + "bounds": { + "minlat": 56.0610221, + "minlon": 10.2114041, + "maxlat": 56.0610778, + "maxlon": 10.2114736 + }, + "geometry": [ + { "lat": 56.0610261, "lon": 10.2114736 }, + { "lat": 56.0610221, "lon": 10.2114155 }, + { "lat": 56.0610738, "lon": 10.2114041 }, + { "lat": 56.0610778, "lon": 10.2114621 }, + { "lat": 56.0610261, "lon": 10.2114736 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493360447, + "bounds": { + "minlat": 56.0610187, + "minlon": 10.2113506, + "maxlat": 56.0610738, + "maxlon": 10.2114155 + }, + "geometry": [ + { "lat": 56.0610221, "lon": 10.2114155 }, + { "lat": 56.0610187, "lon": 10.2113623 }, + { "lat": 56.0610699, "lon": 10.2113506 }, + { "lat": 56.0610738, "lon": 10.2114041 }, + { "lat": 56.0610221, "lon": 10.2114155 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493362902, + "bounds": { + "minlat": 56.1395124, + "minlon": 10.1927677, + "maxlat": 56.1395716, + "maxlon": 10.1928112 + }, + "geometry": [ + { "lat": 56.1395698, "lon": 10.1927677 }, + { "lat": 56.1395124, "lon": 10.1927778 }, + { "lat": 56.1395143, "lon": 10.1928112 }, + { "lat": 56.1395716, "lon": 10.1928011 }, + { "lat": 56.1395698, "lon": 10.1927677 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493363453, + "bounds": { + "minlat": 56.1405569, + "minlon": 10.1924197, + "maxlat": 56.1406089, + "maxlon": 10.1924949 + }, + "geometry": [ + { "lat": 56.1405983, "lon": 10.1924197 }, + { "lat": 56.1406089, "lon": 10.1924567 }, + { "lat": 56.1405676, "lon": 10.1924949 }, + { "lat": 56.1405569, "lon": 10.1924578 }, + { "lat": 56.1405983, "lon": 10.1924197 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493363649, + "bounds": { + "minlat": 56.1403183, + "minlon": 10.1924755, + "maxlat": 56.1403647, + "maxlon": 10.1925244 + }, + "geometry": [ + { "lat": 56.1403604, "lon": 10.1924755 }, + { "lat": 56.1403647, "lon": 10.1925026 }, + { "lat": 56.1403227, "lon": 10.1925244 }, + { "lat": 56.1403183, "lon": 10.1924973 }, + { "lat": 56.1403604, "lon": 10.1924755 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493363930, + "bounds": { + "minlat": 56.1420576, + "minlon": 10.1930653, + "maxlat": 56.1421570, + "maxlon": 10.1931594 + }, + "geometry": [ + { "lat": 56.1421570, "lon": 10.1930653 }, + { "lat": 56.1421201, "lon": 10.1930733 }, + { "lat": 56.1420891, "lon": 10.1930800 }, + { "lat": 56.1420576, "lon": 10.1931594 }, + { "lat": 56.1420901, "lon": 10.1931518 }, + { "lat": 56.1421278, "lon": 10.1931430 }, + { "lat": 56.1421570, "lon": 10.1930653 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493363931, + "bounds": { + "minlat": 56.1420901, + "minlon": 10.1930653, + "maxlat": 56.1421570, + "maxlon": 10.1931518 + }, + "geometry": [ + { "lat": 56.1421570, "lon": 10.1930653 }, + { "lat": 56.1421278, "lon": 10.1931430 }, + { "lat": 56.1420901, "lon": 10.1931518 }, + { "lat": 56.1421201, "lon": 10.1930733 }, + { "lat": 56.1421570, "lon": 10.1930653 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493363932, + "bounds": { + "minlat": 56.1420576, + "minlon": 10.1930733, + "maxlat": 56.1421201, + "maxlon": 10.1931594 + }, + "geometry": [ + { "lat": 56.1420901, "lon": 10.1931518 }, + { "lat": 56.1420576, "lon": 10.1931594 }, + { "lat": 56.1420891, "lon": 10.1930800 }, + { "lat": 56.1421201, "lon": 10.1930733 }, + { "lat": 56.1420901, "lon": 10.1931518 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493363973, + "bounds": { + "minlat": 56.1425271, + "minlon": 10.1930056, + "maxlat": 56.1425778, + "maxlon": 10.1930528 + }, + "geometry": [ + { "lat": 56.1425757, "lon": 10.1930056 }, + { "lat": 56.1425778, "lon": 10.1930443 }, + { "lat": 56.1425292, "lon": 10.1930528 }, + { "lat": 56.1425271, "lon": 10.1930141 }, + { "lat": 56.1425757, "lon": 10.1930056 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493391697, + "bounds": { + "minlat": 56.1150383, + "minlon": 10.1917067, + "maxlat": 56.1150574, + "maxlon": 10.1917872 + }, + "geometry": [ + { "lat": 56.1150383, "lon": 10.1917862 }, + { "lat": 56.1150561, "lon": 10.1917872 }, + { "lat": 56.1150574, "lon": 10.1917076 }, + { "lat": 56.1150397, "lon": 10.1917067 }, + { "lat": 56.1150383, "lon": 10.1917862 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493391847, + "bounds": { + "minlat": 56.1175062, + "minlon": 10.1948910, + "maxlat": 56.1175555, + "maxlon": 10.1949971 + }, + "geometry": [ + { "lat": 56.1175062, "lon": 10.1949777 }, + { "lat": 56.1175440, "lon": 10.1949971 }, + { "lat": 56.1175555, "lon": 10.1949068 }, + { "lat": 56.1175182, "lon": 10.1948910 }, + { "lat": 56.1175062, "lon": 10.1949777 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493392001, + "bounds": { + "minlat": 56.1200264, + "minlon": 10.1812053, + "maxlat": 56.1200835, + "maxlon": 10.1812519 + }, + "geometry": [ + { "lat": 56.1200835, "lon": 10.1812494 }, + { "lat": 56.1200828, "lon": 10.1812053 }, + { "lat": 56.1200264, "lon": 10.1812076 }, + { "lat": 56.1200270, "lon": 10.1812519 }, + { "lat": 56.1200835, "lon": 10.1812494 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493392305, + "bounds": { + "minlat": 56.1208831, + "minlon": 10.1779799, + "maxlat": 56.1209134, + "maxlon": 10.1780706 + }, + "geometry": [ + { "lat": 56.1208831, "lon": 10.1779817 }, + { "lat": 56.1208855, "lon": 10.1780706 }, + { "lat": 56.1209134, "lon": 10.1780694 }, + { "lat": 56.1209114, "lon": 10.1779799 }, + { "lat": 56.1208831, "lon": 10.1779817 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493392828, + "bounds": { + "minlat": 56.1176667, + "minlon": 10.1736299, + "maxlat": 56.1176907, + "maxlon": 10.1737133 + }, + "geometry": [ + { "lat": 56.1176690, "lon": 10.1737133 }, + { "lat": 56.1176667, "lon": 10.1736317 }, + { "lat": 56.1176892, "lon": 10.1736299 }, + { "lat": 56.1176907, "lon": 10.1737127 }, + { "lat": 56.1176690, "lon": 10.1737133 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493394992, + "bounds": { + "minlat": 56.1174513, + "minlon": 10.1729956, + "maxlat": 56.1174897, + "maxlon": 10.1730889 + }, + "geometry": [ + { "lat": 56.1174513, "lon": 10.1729981 }, + { "lat": 56.1174877, "lon": 10.1729956 }, + { "lat": 56.1174897, "lon": 10.1730863 }, + { "lat": 56.1174533, "lon": 10.1730889 }, + { "lat": 56.1174513, "lon": 10.1729981 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493395332, + "bounds": { + "minlat": 56.1157057, + "minlon": 10.1768678, + "maxlat": 56.1157452, + "maxlon": 10.1769522 + }, + "geometry": [ + { "lat": 56.1157447, "lon": 10.1769236 }, + { "lat": 56.1157057, "lon": 10.1769522 }, + { "lat": 56.1157061, "lon": 10.1768941 }, + { "lat": 56.1157452, "lon": 10.1768678 }, + { "lat": 56.1157447, "lon": 10.1769236 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493395333, + "bounds": { + "minlat": 56.1157041, + "minlon": 10.1771068, + "maxlat": 56.1157430, + "maxlon": 10.1771705 + }, + "geometry": [ + { "lat": 56.1157430, "lon": 10.1771068 }, + { "lat": 56.1157043, "lon": 10.1771371 }, + { "lat": 56.1157041, "lon": 10.1771705 }, + { "lat": 56.1157427, "lon": 10.1771409 }, + { "lat": 56.1157430, "lon": 10.1771068 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493395425, + "bounds": { + "minlat": 56.1297055, + "minlon": 10.1659957, + "maxlat": 56.1297500, + "maxlon": 10.1660837 + }, + "geometry": [ + { "lat": 56.1297350, "lon": 10.1659957 }, + { "lat": 56.1297500, "lon": 10.1660171 }, + { "lat": 56.1297205, "lon": 10.1660837 }, + { "lat": 56.1297055, "lon": 10.1660624 }, + { "lat": 56.1297350, "lon": 10.1659957 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493401107, + "bounds": { + "minlat": 56.1497679, + "minlon": 10.1356752, + "maxlat": 56.1498051, + "maxlon": 10.1357579 + }, + "geometry": [ + { "lat": 56.1497688, "lon": 10.1356974 }, + { "lat": 56.1497679, "lon": 10.1357579 }, + { "lat": 56.1498050, "lon": 10.1357579 }, + { "lat": 56.1498051, "lon": 10.1356752 }, + { "lat": 56.1497688, "lon": 10.1356974 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493401234, + "bounds": { + "minlat": 56.1614576, + "minlon": 10.1337738, + "maxlat": 56.1615142, + "maxlon": 10.1338183 + }, + "geometry": [ + { "lat": 56.1615118, "lon": 10.1337738 }, + { "lat": 56.1615142, "lon": 10.1338043 }, + { "lat": 56.1614601, "lon": 10.1338183 }, + { "lat": 56.1614576, "lon": 10.1337877 }, + { "lat": 56.1615118, "lon": 10.1337738 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493401383, + "bounds": { + "minlat": 56.1494112, + "minlon": 10.1574419, + "maxlat": 56.1494664, + "maxlon": 10.1575232 + }, + "geometry": [ + { "lat": 56.1494112, "lon": 10.1574865 }, + { "lat": 56.1494229, "lon": 10.1575232 }, + { "lat": 56.1494664, "lon": 10.1574787 }, + { "lat": 56.1494547, "lon": 10.1574419 }, + { "lat": 56.1494112, "lon": 10.1574865 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493401838, + "bounds": { + "minlat": 56.1484156, + "minlon": 10.1585573, + "maxlat": 56.1485096, + "maxlon": 10.1586837 + }, + "geometry": [ + { "lat": 56.1484991, "lon": 10.1585573 }, + { "lat": 56.1484555, "lon": 10.1586083 }, + { "lat": 56.1484156, "lon": 10.1586549 }, + { "lat": 56.1484261, "lon": 10.1586837 }, + { "lat": 56.1484662, "lon": 10.1586368 }, + { "lat": 56.1485096, "lon": 10.1585861 }, + { "lat": 56.1484991, "lon": 10.1585573 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493401839, + "bounds": { + "minlat": 56.1484555, + "minlon": 10.1585573, + "maxlat": 56.1485096, + "maxlon": 10.1586368 + }, + "geometry": [ + { "lat": 56.1484555, "lon": 10.1586083 }, + { "lat": 56.1484662, "lon": 10.1586368 }, + { "lat": 56.1485096, "lon": 10.1585861 }, + { "lat": 56.1484991, "lon": 10.1585573 }, + { "lat": 56.1484555, "lon": 10.1586083 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493401840, + "bounds": { + "minlat": 56.1484156, + "minlon": 10.1586083, + "maxlat": 56.1484662, + "maxlon": 10.1586837 + }, + "geometry": [ + { "lat": 56.1484555, "lon": 10.1586083 }, + { "lat": 56.1484156, "lon": 10.1586549 }, + { "lat": 56.1484261, "lon": 10.1586837 }, + { "lat": 56.1484662, "lon": 10.1586368 }, + { "lat": 56.1484555, "lon": 10.1586083 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493401921, + "bounds": { + "minlat": 56.1480620, + "minlon": 10.1632112, + "maxlat": 56.1481193, + "maxlon": 10.1632825 + }, + "geometry": [ + { "lat": 56.1480697, "lon": 10.1632825 }, + { "lat": 56.1480620, "lon": 10.1632533 }, + { "lat": 56.1481116, "lon": 10.1632112 }, + { "lat": 56.1481193, "lon": 10.1632404 }, + { "lat": 56.1480697, "lon": 10.1632825 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493404934, + "bounds": { + "minlat": 56.1697517, + "minlon": 10.2177061, + "maxlat": 56.1697760, + "maxlon": 10.2177662 + }, + "geometry": [ + { "lat": 56.1697596, "lon": 10.2177061 }, + { "lat": 56.1697760, "lon": 10.2177141 }, + { "lat": 56.1697681, "lon": 10.2177662 }, + { "lat": 56.1697517, "lon": 10.2177582 }, + { "lat": 56.1697596, "lon": 10.2177061 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493432244, + "bounds": { + "minlat": 56.1619201, + "minlon": 10.2128741, + "maxlat": 56.1619704, + "maxlon": 10.2129416 + }, + "geometry": [ + { "lat": 56.1619201, "lon": 10.2128862 }, + { "lat": 56.1619677, "lon": 10.2128741 }, + { "lat": 56.1619704, "lon": 10.2129295 }, + { "lat": 56.1619239, "lon": 10.2129416 }, + { "lat": 56.1619201, "lon": 10.2128862 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493432768, + "bounds": { + "minlat": 56.1621421, + "minlon": 10.2135693, + "maxlat": 56.1622101, + "maxlon": 10.2136775 + }, + "geometry": [ + { "lat": 56.1621549, "lon": 10.2135693 }, + { "lat": 56.1621421, "lon": 10.2135959 }, + { "lat": 56.1621967, "lon": 10.2136775 }, + { "lat": 56.1622101, "lon": 10.2136527 }, + { "lat": 56.1621549, "lon": 10.2135693 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493432890, + "bounds": { + "minlat": 56.1625395, + "minlon": 10.2145779, + "maxlat": 56.1625792, + "maxlon": 10.2146641 + }, + "geometry": [ + { "lat": 56.1625792, "lon": 10.2145943 }, + { "lat": 56.1625656, "lon": 10.2145779 }, + { "lat": 56.1625395, "lon": 10.2146478 }, + { "lat": 56.1625531, "lon": 10.2146641 }, + { "lat": 56.1625792, "lon": 10.2145943 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493432994, + "bounds": { + "minlat": 56.1618750, + "minlon": 10.2154013, + "maxlat": 56.1619372, + "maxlon": 10.2154695 + }, + "geometry": [ + { "lat": 56.1619307, "lon": 10.2154695 }, + { "lat": 56.1619372, "lon": 10.2154339 }, + { "lat": 56.1618815, "lon": 10.2154013 }, + { "lat": 56.1618750, "lon": 10.2154369 }, + { "lat": 56.1619307, "lon": 10.2154695 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493434278, + "bounds": { + "minlat": 56.1612867, + "minlon": 10.2150374, + "maxlat": 56.1614641, + "maxlon": 10.2151721 + }, + "geometry": [ + { "lat": 56.1614641, "lon": 10.2151444 }, + { "lat": 56.1614588, "lon": 10.2151721 }, + { "lat": 56.1614038, "lon": 10.2151379 }, + { "lat": 56.1613467, "lon": 10.2151023 }, + { "lat": 56.1612867, "lon": 10.2150650 }, + { "lat": 56.1612920, "lon": 10.2150374 }, + { "lat": 56.1613521, "lon": 10.2150748 }, + { "lat": 56.1614092, "lon": 10.2151103 }, + { "lat": 56.1614641, "lon": 10.2151444 } + ], + "tags": { + "amenity": "parking", + "capacity": "3", + "capacity:disabled": "3", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493434279, + "bounds": { + "minlat": 56.1614038, + "minlon": 10.2151103, + "maxlat": 56.1614641, + "maxlon": 10.2151721 + }, + "geometry": [ + { "lat": 56.1614588, "lon": 10.2151721 }, + { "lat": 56.1614038, "lon": 10.2151379 }, + { "lat": 56.1614092, "lon": 10.2151103 }, + { "lat": 56.1614641, "lon": 10.2151444 }, + { "lat": 56.1614588, "lon": 10.2151721 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493434280, + "bounds": { + "minlat": 56.1613467, + "minlon": 10.2150748, + "maxlat": 56.1614092, + "maxlon": 10.2151379 + }, + "geometry": [ + { "lat": 56.1614038, "lon": 10.2151379 }, + { "lat": 56.1613467, "lon": 10.2151023 }, + { "lat": 56.1613521, "lon": 10.2150748 }, + { "lat": 56.1614092, "lon": 10.2151103 }, + { "lat": 56.1614038, "lon": 10.2151379 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493434281, + "bounds": { + "minlat": 56.1612867, + "minlon": 10.2150374, + "maxlat": 56.1613521, + "maxlon": 10.2151023 + }, + "geometry": [ + { "lat": 56.1613467, "lon": 10.2151023 }, + { "lat": 56.1612867, "lon": 10.2150650 }, + { "lat": 56.1612920, "lon": 10.2150374 }, + { "lat": 56.1613521, "lon": 10.2150748 }, + { "lat": 56.1613467, "lon": 10.2151023 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493434900, + "bounds": { + "minlat": 56.1581876, + "minlon": 10.2128458, + "maxlat": 56.1582505, + "maxlon": 10.2130480 + }, + "geometry": [ + { "lat": 56.1581876, "lon": 10.2130327 }, + { "lat": 56.1582091, "lon": 10.2130480 }, + { "lat": 56.1582302, "lon": 10.2129525 }, + { "lat": 56.1582505, "lon": 10.2128611 }, + { "lat": 56.1582290, "lon": 10.2128458 }, + { "lat": 56.1582089, "lon": 10.2129368 }, + { "lat": 56.1581876, "lon": 10.2130327 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493434901, + "bounds": { + "minlat": 56.1581876, + "minlon": 10.2129368, + "maxlat": 56.1582302, + "maxlon": 10.2130480 + }, + "geometry": [ + { "lat": 56.1581876, "lon": 10.2130327 }, + { "lat": 56.1582089, "lon": 10.2129368 }, + { "lat": 56.1582302, "lon": 10.2129525 }, + { "lat": 56.1582091, "lon": 10.2130480 }, + { "lat": 56.1581876, "lon": 10.2130327 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493434902, + "bounds": { + "minlat": 56.1582089, + "minlon": 10.2128458, + "maxlat": 56.1582505, + "maxlon": 10.2129525 + }, + "geometry": [ + { "lat": 56.1582089, "lon": 10.2129368 }, + { "lat": 56.1582290, "lon": 10.2128458 }, + { "lat": 56.1582505, "lon": 10.2128611 }, + { "lat": 56.1582302, "lon": 10.2129525 }, + { "lat": 56.1582089, "lon": 10.2129368 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493435644, + "bounds": { + "minlat": 56.1601515, + "minlon": 10.2115693, + "maxlat": 56.1602380, + "maxlon": 10.2116966 + }, + "geometry": [ + { "lat": 56.1601644, "lon": 10.2115693 }, + { "lat": 56.1601515, "lon": 10.2116020 }, + { "lat": 56.1601883, "lon": 10.2116508 }, + { "lat": 56.1602254, "lon": 10.2116966 }, + { "lat": 56.1602380, "lon": 10.2116636 }, + { "lat": 56.1602010, "lon": 10.2116177 }, + { "lat": 56.1601644, "lon": 10.2115693 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493435645, + "bounds": { + "minlat": 56.1601883, + "minlon": 10.2116177, + "maxlat": 56.1602380, + "maxlon": 10.2116966 + }, + "geometry": [ + { "lat": 56.1602254, "lon": 10.2116966 }, + { "lat": 56.1601883, "lon": 10.2116508 }, + { "lat": 56.1602010, "lon": 10.2116177 }, + { "lat": 56.1602380, "lon": 10.2116636 }, + { "lat": 56.1602254, "lon": 10.2116966 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493435646, + "bounds": { + "minlat": 56.1601515, + "minlon": 10.2115693, + "maxlat": 56.1602010, + "maxlon": 10.2116508 + }, + "geometry": [ + { "lat": 56.1601883, "lon": 10.2116508 }, + { "lat": 56.1601515, "lon": 10.2116020 }, + { "lat": 56.1601644, "lon": 10.2115693 }, + { "lat": 56.1602010, "lon": 10.2116177 }, + { "lat": 56.1601883, "lon": 10.2116508 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493436138, + "bounds": { + "minlat": 56.1600610, + "minlon": 10.2114464, + "maxlat": 56.1601095, + "maxlon": 10.2115257 + }, + "geometry": [ + { "lat": 56.1600724, "lon": 10.2114464 }, + { "lat": 56.1600610, "lon": 10.2114718 }, + { "lat": 56.1600981, "lon": 10.2115257 }, + { "lat": 56.1601095, "lon": 10.2115003 }, + { "lat": 56.1600724, "lon": 10.2114464 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493476155, + "bounds": { + "minlat": 56.1689655, + "minlon": 10.1910191, + "maxlat": 56.1690257, + "maxlon": 10.1911882 + }, + "geometry": [ + { "lat": 56.1689785, "lon": 10.1910191 }, + { "lat": 56.1690022, "lon": 10.1910687 }, + { "lat": 56.1690257, "lon": 10.1911176 }, + { "lat": 56.1690107, "lon": 10.1911882 }, + { "lat": 56.1689888, "lon": 10.1911454 }, + { "lat": 56.1689655, "lon": 10.1911002 }, + { "lat": 56.1689785, "lon": 10.1910191 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493476156, + "bounds": { + "minlat": 56.1689888, + "minlon": 10.1910687, + "maxlat": 56.1690257, + "maxlon": 10.1911882 + }, + "geometry": [ + { "lat": 56.1690022, "lon": 10.1910687 }, + { "lat": 56.1689888, "lon": 10.1911454 }, + { "lat": 56.1690107, "lon": 10.1911882 }, + { "lat": 56.1690257, "lon": 10.1911176 }, + { "lat": 56.1690022, "lon": 10.1910687 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493476157, + "bounds": { + "minlat": 56.1689655, + "minlon": 10.1910191, + "maxlat": 56.1690022, + "maxlon": 10.1911454 + }, + "geometry": [ + { "lat": 56.1689785, "lon": 10.1910191 }, + { "lat": 56.1689655, "lon": 10.1911002 }, + { "lat": 56.1689888, "lon": 10.1911454 }, + { "lat": 56.1690022, "lon": 10.1910687 }, + { "lat": 56.1689785, "lon": 10.1910191 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493476341, + "bounds": { + "minlat": 56.1679420, + "minlon": 10.1891553, + "maxlat": 56.1679900, + "maxlon": 10.1892394 + }, + "geometry": [ + { "lat": 56.1679732, "lon": 10.1892394 }, + { "lat": 56.1679900, "lon": 10.1892058 }, + { "lat": 56.1679588, "lon": 10.1891553 }, + { "lat": 56.1679420, "lon": 10.1891890 }, + { "lat": 56.1679732, "lon": 10.1892394 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493477493, + "bounds": { + "minlat": 56.1686174, + "minlon": 10.1878362, + "maxlat": 56.1686521, + "maxlon": 10.1879555 + }, + "geometry": [ + { "lat": 56.1686174, "lon": 10.1878571 }, + { "lat": 56.1686354, "lon": 10.1879555 }, + { "lat": 56.1686521, "lon": 10.1879338 }, + { "lat": 56.1686344, "lon": 10.1878362 }, + { "lat": 56.1686174, "lon": 10.1878571 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493477494, + "bounds": { + "minlat": 56.1685999, + "minlon": 10.1878571, + "maxlat": 56.1686354, + "maxlon": 10.1879770 + }, + "geometry": [ + { "lat": 56.1686174, "lon": 10.1878571 }, + { "lat": 56.1685999, "lon": 10.1878786 }, + { "lat": 56.1686190, "lon": 10.1879770 }, + { "lat": 56.1686354, "lon": 10.1879555 }, + { "lat": 56.1686174, "lon": 10.1878571 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493506589, + "bounds": { + "minlat": 56.1472094, + "minlon": 10.2040236, + "maxlat": 56.1472327, + "maxlon": 10.2041288 + }, + "geometry": [ + { "lat": 56.1472327, "lon": 10.2041258 }, + { "lat": 56.1472148, "lon": 10.2041288 }, + { "lat": 56.1472094, "lon": 10.2040267 }, + { "lat": 56.1472273, "lon": 10.2040236 }, + { "lat": 56.1472327, "lon": 10.2041258 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493506910, + "bounds": { + "minlat": 56.1472189, + "minlon": 10.2042507, + "maxlat": 56.1472403, + "maxlon": 10.2043352 + }, + "geometry": [ + { "lat": 56.1472360, "lon": 10.2042507 }, + { "lat": 56.1472189, "lon": 10.2042536 }, + { "lat": 56.1472231, "lon": 10.2043352 }, + { "lat": 56.1472403, "lon": 10.2043323 }, + { "lat": 56.1472360, "lon": 10.2042507 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493559343, + "bounds": { + "minlat": 56.1535744, + "minlon": 10.2087517, + "maxlat": 56.1536344, + "maxlon": 10.2089564 + }, + "geometry": [ + { "lat": 56.1535928, "lon": 10.2089564 }, + { "lat": 56.1536137, "lon": 10.2088599 }, + { "lat": 56.1536344, "lon": 10.2087645 }, + { "lat": 56.1536161, "lon": 10.2087517 }, + { "lat": 56.1535952, "lon": 10.2088478 }, + { "lat": 56.1535744, "lon": 10.2089435 }, + { "lat": 56.1535928, "lon": 10.2089564 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493559344, + "bounds": { + "minlat": 56.1535744, + "minlon": 10.2088478, + "maxlat": 56.1536137, + "maxlon": 10.2089564 + }, + "geometry": [ + { "lat": 56.1535928, "lon": 10.2089564 }, + { "lat": 56.1536137, "lon": 10.2088599 }, + { "lat": 56.1535952, "lon": 10.2088478 }, + { "lat": 56.1535744, "lon": 10.2089435 }, + { "lat": 56.1535928, "lon": 10.2089564 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493559345, + "bounds": { + "minlat": 56.1535952, + "minlon": 10.2087517, + "maxlat": 56.1536344, + "maxlon": 10.2088599 + }, + "geometry": [ + { "lat": 56.1536137, "lon": 10.2088599 }, + { "lat": 56.1536344, "lon": 10.2087645 }, + { "lat": 56.1536161, "lon": 10.2087517 }, + { "lat": 56.1535952, "lon": 10.2088478 }, + { "lat": 56.1536137, "lon": 10.2088599 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493559438, + "bounds": { + "minlat": 56.1527892, + "minlon": 10.2085017, + "maxlat": 56.1528804, + "maxlon": 10.2085873 + }, + "geometry": [ + { "lat": 56.1527892, "lon": 10.2085311 }, + { "lat": 56.1527952, "lon": 10.2085017 }, + { "lat": 56.1528375, "lon": 10.2085295 }, + { "lat": 56.1528804, "lon": 10.2085578 }, + { "lat": 56.1528743, "lon": 10.2085873 }, + { "lat": 56.1528310, "lon": 10.2085587 }, + { "lat": 56.1527892, "lon": 10.2085311 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493559439, + "bounds": { + "minlat": 56.1528310, + "minlon": 10.2085295, + "maxlat": 56.1528804, + "maxlon": 10.2085873 + }, + "geometry": [ + { "lat": 56.1528743, "lon": 10.2085873 }, + { "lat": 56.1528310, "lon": 10.2085587 }, + { "lat": 56.1528375, "lon": 10.2085295 }, + { "lat": 56.1528804, "lon": 10.2085578 }, + { "lat": 56.1528743, "lon": 10.2085873 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493559440, + "bounds": { + "minlat": 56.1527892, + "minlon": 10.2085017, + "maxlat": 56.1528375, + "maxlon": 10.2085587 + }, + "geometry": [ + { "lat": 56.1528310, "lon": 10.2085587 }, + { "lat": 56.1527892, "lon": 10.2085311 }, + { "lat": 56.1527952, "lon": 10.2085017 }, + { "lat": 56.1528375, "lon": 10.2085295 }, + { "lat": 56.1528310, "lon": 10.2085587 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493559767, + "bounds": { + "minlat": 56.1558256, + "minlon": 10.2077139, + "maxlat": 56.1558679, + "maxlon": 10.2077802 + }, + "geometry": [ + { "lat": 56.1558574, "lon": 10.2077139 }, + { "lat": 56.1558256, "lon": 10.2077518 }, + { "lat": 56.1558360, "lon": 10.2077802 }, + { "lat": 56.1558679, "lon": 10.2077422 }, + { "lat": 56.1558574, "lon": 10.2077139 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493559928, + "bounds": { + "minlat": 56.1577554, + "minlon": 10.2076736, + "maxlat": 56.1578466, + "maxlon": 10.2077663 + }, + "geometry": [ + { "lat": 56.1577554, "lon": 10.2077050 }, + { "lat": 56.1577625, "lon": 10.2076736 }, + { "lat": 56.1578052, "lon": 10.2077047 }, + { "lat": 56.1578466, "lon": 10.2077348 }, + { "lat": 56.1578395, "lon": 10.2077663 }, + { "lat": 56.1577985, "lon": 10.2077364 }, + { "lat": 56.1577554, "lon": 10.2077050 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493559929, + "bounds": { + "minlat": 56.1577985, + "minlon": 10.2077047, + "maxlat": 56.1578466, + "maxlon": 10.2077663 + }, + "geometry": [ + { "lat": 56.1578395, "lon": 10.2077663 }, + { "lat": 56.1577985, "lon": 10.2077364 }, + { "lat": 56.1578052, "lon": 10.2077047 }, + { "lat": 56.1578466, "lon": 10.2077348 }, + { "lat": 56.1578395, "lon": 10.2077663 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493559930, + "bounds": { + "minlat": 56.1577554, + "minlon": 10.2076736, + "maxlat": 56.1578052, + "maxlon": 10.2077364 + }, + "geometry": [ + { "lat": 56.1577985, "lon": 10.2077364 }, + { "lat": 56.1577554, "lon": 10.2077050 }, + { "lat": 56.1577625, "lon": 10.2076736 }, + { "lat": 56.1578052, "lon": 10.2077047 }, + { "lat": 56.1577985, "lon": 10.2077364 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493560794, + "bounds": { + "minlat": 56.1434289, + "minlon": 10.2054937, + "maxlat": 56.1434743, + "maxlon": 10.2055246 + }, + "geometry": [ + { "lat": 56.1434739, "lon": 10.2054937 }, + { "lat": 56.1434743, "lon": 10.2055228 }, + { "lat": 56.1434292, "lon": 10.2055246 }, + { "lat": 56.1434289, "lon": 10.2054955 }, + { "lat": 56.1434739, "lon": 10.2054937 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493560949, + "bounds": { + "minlat": 56.1438697, + "minlon": 10.2054514, + "maxlat": 56.1439231, + "maxlon": 10.2054879 + }, + "geometry": [ + { "lat": 56.1439226, "lon": 10.2054514 }, + { "lat": 56.1438697, "lon": 10.2054539 }, + { "lat": 56.1438702, "lon": 10.2054879 }, + { "lat": 56.1439231, "lon": 10.2054854 }, + { "lat": 56.1439226, "lon": 10.2054514 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493561195, + "bounds": { + "minlat": 56.1440747, + "minlon": 10.2044120, + "maxlat": 56.1441628, + "maxlon": 10.2044535 + }, + "geometry": [ + { "lat": 56.1440747, "lon": 10.2044447 }, + { "lat": 56.1440756, "lon": 10.2044120 }, + { "lat": 56.1441223, "lon": 10.2044163 }, + { "lat": 56.1441628, "lon": 10.2044208 }, + { "lat": 56.1441618, "lon": 10.2044535 }, + { "lat": 56.1441214, "lon": 10.2044490 }, + { "lat": 56.1440747, "lon": 10.2044447 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side", + "surface": "asphalt" + } +}, +{ + "type": "way", + "id": 1493561196, + "bounds": { + "minlat": 56.1441214, + "minlon": 10.2044163, + "maxlat": 56.1441628, + "maxlon": 10.2044535 + }, + "geometry": [ + { "lat": 56.1441628, "lon": 10.2044208 }, + { "lat": 56.1441618, "lon": 10.2044535 }, + { "lat": 56.1441214, "lon": 10.2044490 }, + { "lat": 56.1441223, "lon": 10.2044163 }, + { "lat": 56.1441628, "lon": 10.2044208 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493561197, + "bounds": { + "minlat": 56.1440747, + "minlon": 10.2044120, + "maxlat": 56.1441223, + "maxlon": 10.2044490 + }, + "geometry": [ + { "lat": 56.1441223, "lon": 10.2044163 }, + { "lat": 56.1441214, "lon": 10.2044490 }, + { "lat": 56.1440747, "lon": 10.2044447 }, + { "lat": 56.1440756, "lon": 10.2044120 }, + { "lat": 56.1441223, "lon": 10.2044163 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493561534, + "bounds": { + "minlat": 56.1454479, + "minlon": 10.2046077, + "maxlat": 56.1454874, + "maxlon": 10.2047635 + }, + "geometry": [ + { "lat": 56.1454479, "lon": 10.2046798 }, + { "lat": 56.1454800, "lon": 10.2047635 }, + { "lat": 56.1454874, "lon": 10.2046922 }, + { "lat": 56.1454553, "lon": 10.2046077 }, + { "lat": 56.1454479, "lon": 10.2046798 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493572498, + "bounds": { + "minlat": 56.1540698, + "minlon": 10.2016809, + "maxlat": 56.1541563, + "maxlon": 10.2018019 + }, + "geometry": [ + { "lat": 56.1540698, "lon": 10.2017117 }, + { "lat": 56.1540812, "lon": 10.2016809 }, + { "lat": 56.1541186, "lon": 10.2017258 }, + { "lat": 56.1541563, "lon": 10.2017712 }, + { "lat": 56.1541449, "lon": 10.2018019 }, + { "lat": 56.1541079, "lon": 10.2017575 }, + { "lat": 56.1540698, "lon": 10.2017117 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493572499, + "bounds": { + "minlat": 56.1541079, + "minlon": 10.2017258, + "maxlat": 56.1541563, + "maxlon": 10.2018019 + }, + "geometry": [ + { "lat": 56.1541449, "lon": 10.2018019 }, + { "lat": 56.1541079, "lon": 10.2017575 }, + { "lat": 56.1541186, "lon": 10.2017258 }, + { "lat": 56.1541563, "lon": 10.2017712 }, + { "lat": 56.1541449, "lon": 10.2018019 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493572500, + "bounds": { + "minlat": 56.1540698, + "minlon": 10.2016809, + "maxlat": 56.1541186, + "maxlon": 10.2017575 + }, + "geometry": [ + { "lat": 56.1541186, "lon": 10.2017258 }, + { "lat": 56.1540812, "lon": 10.2016809 }, + { "lat": 56.1540698, "lon": 10.2017117 }, + { "lat": 56.1541079, "lon": 10.2017575 }, + { "lat": 56.1541186, "lon": 10.2017258 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493573063, + "bounds": { + "minlat": 56.1556438, + "minlon": 10.2033143, + "maxlat": 56.1557299, + "maxlon": 10.2033908 + }, + "geometry": [ + { "lat": 56.1557224, "lon": 10.2033143 }, + { "lat": 56.1556874, "lon": 10.2033480 }, + { "lat": 56.1556438, "lon": 10.2033620 }, + { "lat": 56.1556467, "lon": 10.2033908 }, + { "lat": 56.1556912, "lon": 10.2033765 }, + { "lat": 56.1557299, "lon": 10.2033393 }, + { "lat": 56.1557224, "lon": 10.2033143 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493573064, + "bounds": { + "minlat": 56.1556874, + "minlon": 10.2033143, + "maxlat": 56.1557299, + "maxlon": 10.2033765 + }, + "geometry": [ + { "lat": 56.1557224, "lon": 10.2033143 }, + { "lat": 56.1557299, "lon": 10.2033393 }, + { "lat": 56.1556912, "lon": 10.2033765 }, + { "lat": 56.1556874, "lon": 10.2033480 }, + { "lat": 56.1557224, "lon": 10.2033143 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493573065, + "bounds": { + "minlat": 56.1556438, + "minlon": 10.2033480, + "maxlat": 56.1556912, + "maxlon": 10.2033908 + }, + "geometry": [ + { "lat": 56.1556874, "lon": 10.2033480 }, + { "lat": 56.1556912, "lon": 10.2033765 }, + { "lat": 56.1556467, "lon": 10.2033908 }, + { "lat": 56.1556438, "lon": 10.2033620 }, + { "lat": 56.1556874, "lon": 10.2033480 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493580522, + "bounds": { + "minlat": 56.1462726, + "minlon": 10.1886005, + "maxlat": 56.1463086, + "maxlon": 10.1886697 + }, + "geometry": [ + { "lat": 56.1462944, "lon": 10.1886697 }, + { "lat": 56.1463086, "lon": 10.1886493 }, + { "lat": 56.1462868, "lon": 10.1886005 }, + { "lat": 56.1462726, "lon": 10.1886210 }, + { "lat": 56.1462944, "lon": 10.1886697 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493580823, + "bounds": { + "minlat": 56.1426154, + "minlon": 10.1888433, + "maxlat": 56.1426639, + "maxlon": 10.1888814 + }, + "geometry": [ + { "lat": 56.1426639, "lon": 10.1888481 }, + { "lat": 56.1426628, "lon": 10.1888814 }, + { "lat": 56.1426154, "lon": 10.1888766 }, + { "lat": 56.1426165, "lon": 10.1888433 }, + { "lat": 56.1426639, "lon": 10.1888481 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493615955, + "bounds": { + "minlat": 56.1449121, + "minlon": 10.1991706, + "maxlat": 56.1449693, + "maxlon": 10.1992425 + }, + "geometry": [ + { "lat": 56.1449164, "lon": 10.1992425 }, + { "lat": 56.1449693, "lon": 10.1992260 }, + { "lat": 56.1449644, "lon": 10.1991706 }, + { "lat": 56.1449121, "lon": 10.1991870 }, + { "lat": 56.1449164, "lon": 10.1992425 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493615956, + "bounds": { + "minlat": 56.1449078, + "minlon": 10.1991147, + "maxlat": 56.1449644, + "maxlon": 10.1991870 + }, + "geometry": [ + { "lat": 56.1449121, "lon": 10.1991870 }, + { "lat": 56.1449078, "lon": 10.1991307 }, + { "lat": 56.1449594, "lon": 10.1991147 }, + { "lat": 56.1449644, "lon": 10.1991706 }, + { "lat": 56.1449121, "lon": 10.1991870 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493615957, + "bounds": { + "minlat": 56.1449031, + "minlon": 10.1990595, + "maxlat": 56.1449594, + "maxlon": 10.1991307 + }, + "geometry": [ + { "lat": 56.1449078, "lon": 10.1991307 }, + { "lat": 56.1449031, "lon": 10.1990737 }, + { "lat": 56.1449545, "lon": 10.1990595 }, + { "lat": 56.1449594, "lon": 10.1991147 }, + { "lat": 56.1449078, "lon": 10.1991307 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493616314, + "bounds": { + "minlat": 56.1454587, + "minlon": 10.1986357, + "maxlat": 56.1454980, + "maxlon": 10.1987178 + }, + "geometry": [ + { "lat": 56.1454904, "lon": 10.1986357 }, + { "lat": 56.1454587, "lon": 10.1986466 }, + { "lat": 56.1454663, "lon": 10.1987178 }, + { "lat": 56.1454980, "lon": 10.1987068 }, + { "lat": 56.1454904, "lon": 10.1986357 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493616472, + "bounds": { + "minlat": 56.1456451, + "minlon": 10.1985733, + "maxlat": 56.1456849, + "maxlon": 10.1986574 + }, + "geometry": [ + { "lat": 56.1456451, "lon": 10.1985872 }, + { "lat": 56.1456747, "lon": 10.1985733 }, + { "lat": 56.1456849, "lon": 10.1986435 }, + { "lat": 56.1456553, "lon": 10.1986574 }, + { "lat": 56.1456451, "lon": 10.1985872 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493616703, + "bounds": { + "minlat": 56.1470822, + "minlon": 10.2001084, + "maxlat": 56.1471022, + "maxlon": 10.2001985 + }, + "geometry": [ + { "lat": 56.1471004, "lon": 10.2001084 }, + { "lat": 56.1470822, "lon": 10.2001096 }, + { "lat": 56.1470840, "lon": 10.2001985 }, + { "lat": 56.1471022, "lon": 10.2001973 }, + { "lat": 56.1471004, "lon": 10.2001084 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493616958, + "bounds": { + "minlat": 56.1482274, + "minlon": 10.2001526, + "maxlat": 56.1482874, + "maxlon": 10.2002021 + }, + "geometry": [ + { "lat": 56.1482274, "lon": 10.2001633 }, + { "lat": 56.1482296, "lon": 10.2002021 }, + { "lat": 56.1482874, "lon": 10.2001915 }, + { "lat": 56.1482852, "lon": 10.2001526 }, + { "lat": 56.1482274, "lon": 10.2001633 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493617440, + "bounds": { + "minlat": 56.1482724, + "minlon": 10.1989688, + "maxlat": 56.1483251, + "maxlon": 10.1990132 + }, + "geometry": [ + { "lat": 56.1482724, "lon": 10.1989739 }, + { "lat": 56.1482736, "lon": 10.1990132 }, + { "lat": 56.1483251, "lon": 10.1990081 }, + { "lat": 56.1483239, "lon": 10.1989688 }, + { "lat": 56.1482724, "lon": 10.1989739 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493617856, + "bounds": { + "minlat": 56.1514012, + "minlon": 10.1986736, + "maxlat": 56.1514604, + "maxlon": 10.1987965 + }, + "geometry": [ + { "lat": 56.1514288, "lon": 10.1987965 }, + { "lat": 56.1514445, "lon": 10.1987504 }, + { "lat": 56.1514604, "lon": 10.1987039 }, + { "lat": 56.1514328, "lon": 10.1986736 }, + { "lat": 56.1514171, "lon": 10.1987198 }, + { "lat": 56.1514012, "lon": 10.1987662 }, + { "lat": 56.1514288, "lon": 10.1987965 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493617857, + "bounds": { + "minlat": 56.1514012, + "minlon": 10.1987198, + "maxlat": 56.1514445, + "maxlon": 10.1987965 + }, + "geometry": [ + { "lat": 56.1514288, "lon": 10.1987965 }, + { "lat": 56.1514445, "lon": 10.1987504 }, + { "lat": 56.1514171, "lon": 10.1987198 }, + { "lat": 56.1514012, "lon": 10.1987662 }, + { "lat": 56.1514288, "lon": 10.1987965 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493617858, + "bounds": { + "minlat": 56.1514171, + "minlon": 10.1986736, + "maxlat": 56.1514604, + "maxlon": 10.1987504 + }, + "geometry": [ + { "lat": 56.1514445, "lon": 10.1987504 }, + { "lat": 56.1514604, "lon": 10.1987039 }, + { "lat": 56.1514328, "lon": 10.1986736 }, + { "lat": 56.1514171, "lon": 10.1987198 }, + { "lat": 56.1514445, "lon": 10.1987504 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493618477, + "bounds": { + "minlat": 56.1506760, + "minlon": 10.1974811, + "maxlat": 56.1507821, + "maxlon": 10.1976275 + }, + "geometry": [ + { "lat": 56.1506760, "lon": 10.1975164 }, + { "lat": 56.1506890, "lon": 10.1974811 }, + { "lat": 56.1507334, "lon": 10.1975346 }, + { "lat": 56.1507821, "lon": 10.1975921 }, + { "lat": 56.1507692, "lon": 10.1976275 }, + { "lat": 56.1507204, "lon": 10.1975700 }, + { "lat": 56.1506760, "lon": 10.1975164 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493618478, + "bounds": { + "minlat": 56.1507204, + "minlon": 10.1975346, + "maxlat": 56.1507821, + "maxlon": 10.1976275 + }, + "geometry": [ + { "lat": 56.1507204, "lon": 10.1975700 }, + { "lat": 56.1507334, "lon": 10.1975346 }, + { "lat": 56.1507821, "lon": 10.1975921 }, + { "lat": 56.1507692, "lon": 10.1976275 }, + { "lat": 56.1507204, "lon": 10.1975700 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493618479, + "bounds": { + "minlat": 56.1506760, + "minlon": 10.1974811, + "maxlat": 56.1507334, + "maxlon": 10.1975700 + }, + "geometry": [ + { "lat": 56.1507204, "lon": 10.1975700 }, + { "lat": 56.1506760, "lon": 10.1975164 }, + { "lat": 56.1506890, "lon": 10.1974811 }, + { "lat": 56.1507334, "lon": 10.1975346 }, + { "lat": 56.1507204, "lon": 10.1975700 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493618878, + "bounds": { + "minlat": 56.1505369, + "minlon": 10.1973247, + "maxlat": 56.1505853, + "maxlon": 10.1973999 + }, + "geometry": [ + { "lat": 56.1505369, "lon": 10.1973544 }, + { "lat": 56.1505483, "lon": 10.1973247 }, + { "lat": 56.1505853, "lon": 10.1973702 }, + { "lat": 56.1505739, "lon": 10.1973999 }, + { "lat": 56.1505369, "lon": 10.1973544 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493619320, + "bounds": { + "minlat": 56.1508359, + "minlon": 10.1961279, + "maxlat": 56.1508910, + "maxlon": 10.1961780 + }, + "geometry": [ + { "lat": 56.1508393, "lon": 10.1961279 }, + { "lat": 56.1508359, "lon": 10.1961609 }, + { "lat": 56.1508877, "lon": 10.1961780 }, + { "lat": 56.1508910, "lon": 10.1961450 }, + { "lat": 56.1508393, "lon": 10.1961279 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493619616, + "bounds": { + "minlat": 56.1503139, + "minlon": 10.1958740, + "maxlat": 56.1503858, + "maxlon": 10.1959646 + }, + "geometry": [ + { "lat": 56.1503858, "lon": 10.1959338 }, + { "lat": 56.1503767, "lon": 10.1959646 }, + { "lat": 56.1503139, "lon": 10.1959048 }, + { "lat": 56.1503229, "lon": 10.1958740 }, + { "lat": 56.1503858, "lon": 10.1959338 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493619783, + "bounds": { + "minlat": 56.1501936, + "minlon": 10.1958489, + "maxlat": 56.1502574, + "maxlon": 10.1959337 + }, + "geometry": [ + { "lat": 56.1502034, "lon": 10.1958489 }, + { "lat": 56.1501936, "lon": 10.1958816 }, + { "lat": 56.1502476, "lon": 10.1959337 }, + { "lat": 56.1502574, "lon": 10.1959009 }, + { "lat": 56.1502034, "lon": 10.1958489 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493619952, + "bounds": { + "minlat": 56.1500778, + "minlon": 10.1956303, + "maxlat": 56.1501210, + "maxlon": 10.1957295 + }, + "geometry": [ + { "lat": 56.1500778, "lon": 10.1957119 }, + { "lat": 56.1500947, "lon": 10.1957295 }, + { "lat": 56.1501210, "lon": 10.1956478 }, + { "lat": 56.1501041, "lon": 10.1956303 }, + { "lat": 56.1500778, "lon": 10.1957119 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493635968, + "bounds": { + "minlat": 56.1419583, + "minlon": 10.1955679, + "maxlat": 56.1420262, + "maxlon": 10.1956209 + }, + "geometry": [ + { "lat": 56.1420234, "lon": 10.1955679 }, + { "lat": 56.1420262, "lon": 10.1956051 }, + { "lat": 56.1419611, "lon": 10.1956209 }, + { "lat": 56.1419583, "lon": 10.1955837 }, + { "lat": 56.1420234, "lon": 10.1955679 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493636008, + "bounds": { + "minlat": 56.1418292, + "minlon": 10.1956136, + "maxlat": 56.1418824, + "maxlon": 10.1956541 + }, + "geometry": [ + { "lat": 56.1418292, "lon": 10.1956242 }, + { "lat": 56.1418311, "lon": 10.1956541 }, + { "lat": 56.1418824, "lon": 10.1956435 }, + { "lat": 56.1418805, "lon": 10.1956136 }, + { "lat": 56.1418292, "lon": 10.1956242 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1493941134, + "bounds": { + "minlat": 56.1766646, + "minlon": 10.1372432, + "maxlat": 56.1767228, + "maxlon": 10.1373502 + }, + "geometry": [ + { "lat": 56.1766646, "lon": 10.1373201 }, + { "lat": 56.1767050, "lon": 10.1373502 }, + { "lat": 56.1767228, "lon": 10.1372731 }, + { "lat": 56.1766827, "lon": 10.1372432 }, + { "lat": 56.1766646, "lon": 10.1373201 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493941136, + "bounds": { + "minlat": 56.1763277, + "minlon": 10.1382682, + "maxlat": 56.1763800, + "maxlon": 10.1383496 + }, + "geometry": [ + { "lat": 56.1763277, "lon": 10.1383192 }, + { "lat": 56.1763681, "lon": 10.1383496 }, + { "lat": 56.1763800, "lon": 10.1382986 }, + { "lat": 56.1763396, "lon": 10.1382682 }, + { "lat": 56.1763277, "lon": 10.1383192 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493941137, + "bounds": { + "minlat": 56.1763396, + "minlon": 10.1382146, + "maxlat": 56.1763926, + "maxlon": 10.1382986 + }, + "geometry": [ + { "lat": 56.1763396, "lon": 10.1382682 }, + { "lat": 56.1763519, "lon": 10.1382146 }, + { "lat": 56.1763926, "lon": 10.1382440 }, + { "lat": 56.1763800, "lon": 10.1382986 }, + { "lat": 56.1763396, "lon": 10.1382682 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493941138, + "bounds": { + "minlat": 56.1763519, + "minlon": 10.1381621, + "maxlat": 56.1764042, + "maxlon": 10.1382440 + }, + "geometry": [ + { "lat": 56.1763519, "lon": 10.1382146 }, + { "lat": 56.1763639, "lon": 10.1381621 }, + { "lat": 56.1764042, "lon": 10.1381912 }, + { "lat": 56.1763926, "lon": 10.1382440 }, + { "lat": 56.1763519, "lon": 10.1382146 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493943910, + "bounds": { + "minlat": 56.1770363, + "minlon": 10.1351302, + "maxlat": 56.1773646, + "maxlon": 10.1363811 + }, + "geometry": [ + { "lat": 56.1770363, "lon": 10.1351636 }, + { "lat": 56.1770419, "lon": 10.1354741 }, + { "lat": 56.1770650, "lon": 10.1363184 }, + { "lat": 56.1770666, "lon": 10.1363811 }, + { "lat": 56.1773646, "lon": 10.1363533 }, + { "lat": 56.1773512, "lon": 10.1358961 }, + { "lat": 56.1773499, "lon": 10.1358258 }, + { "lat": 56.1773408, "lon": 10.1355033 }, + { "lat": 56.1773386, "lon": 10.1354412 }, + { "lat": 56.1773309, "lon": 10.1351302 }, + { "lat": 56.1770363, "lon": 10.1351636 } + ], + "tags": { + "amenity": "parking", + "capacity:charging": "22", + "capacity:disabled": "2", + "parking": "surface" + } +}, +{ + "type": "way", + "id": 1493943911, + "bounds": { + "minlat": 56.1772927, + "minlon": 10.1354412, + "maxlat": 56.1773408, + "maxlon": 10.1355069 + }, + "geometry": [ + { "lat": 56.1773386, "lon": 10.1354412 }, + { "lat": 56.1772955, "lon": 10.1354444 }, + { "lat": 56.1772927, "lon": 10.1354447 }, + { "lat": 56.1772942, "lon": 10.1355069 }, + { "lat": 56.1773408, "lon": 10.1355033 }, + { "lat": 56.1773386, "lon": 10.1354412 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1493943912, + "bounds": { + "minlat": 56.1773051, + "minlon": 10.1358258, + "maxlat": 56.1773512, + "maxlon": 10.1358988 + }, + "geometry": [ + { "lat": 56.1773499, "lon": 10.1358258 }, + { "lat": 56.1773474, "lon": 10.1358266 }, + { "lat": 56.1773051, "lon": 10.1358303 }, + { "lat": 56.1773063, "lon": 10.1358988 }, + { "lat": 56.1773512, "lon": 10.1358961 }, + { "lat": 56.1773499, "lon": 10.1358258 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494065170, + "bounds": { + "minlat": 56.1668269, + "minlon": 10.2064103, + "maxlat": 56.1668730, + "maxlon": 10.2064626 + }, + "geometry": [ + { "lat": 56.1668698, "lon": 10.2064103 }, + { "lat": 56.1668269, "lon": 10.2064209 }, + { "lat": 56.1668301, "lon": 10.2064626 }, + { "lat": 56.1668730, "lon": 10.2064520 }, + { "lat": 56.1668698, "lon": 10.2064103 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494065483, + "bounds": { + "minlat": 56.1637511, + "minlon": 10.2073134, + "maxlat": 56.1637937, + "maxlon": 10.2074046 + }, + "geometry": [ + { "lat": 56.1637511, "lon": 10.2073316 }, + { "lat": 56.1637659, "lon": 10.2074046 }, + { "lat": 56.1637937, "lon": 10.2073868 }, + { "lat": 56.1637786, "lon": 10.2073134 }, + { "lat": 56.1637511, "lon": 10.2073316 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494190959, + "bounds": { + "minlat": 56.1758515, + "minlon": 10.1376693, + "maxlat": 56.1765082, + "maxlon": 10.1398226 + }, + "geometry": [ + { "lat": 56.1760532, "lon": 10.1376693 }, + { "lat": 56.1764367, "lon": 10.1379509 }, + { "lat": 56.1763943, "lon": 10.1381698 }, + { "lat": 56.1765082, "lon": 10.1382630 }, + { "lat": 56.1764692, "lon": 10.1384326 }, + { "lat": 56.1764232, "lon": 10.1383983 }, + { "lat": 56.1763330, "lon": 10.1388094 }, + { "lat": 56.1763813, "lon": 10.1388453 }, + { "lat": 56.1763047, "lon": 10.1391871 }, + { "lat": 56.1761575, "lon": 10.1398226 }, + { "lat": 56.1758515, "lon": 10.1396038 }, + { "lat": 56.1760532, "lon": 10.1376693 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "3", + "parking": "surface", + "surface": "paving_stones" + } +}, +{ + "type": "way", + "id": 1494217514, + "bounds": { + "minlat": 56.1754306, + "minlon": 10.1406416, + "maxlat": 56.1754804, + "maxlon": 10.1407072 + }, + "geometry": [ + { "lat": 56.1754306, "lon": 10.1406921 }, + { "lat": 56.1754359, "lon": 10.1406416 }, + { "lat": 56.1754804, "lon": 10.1406566 }, + { "lat": 56.1754751, "lon": 10.1407072 }, + { "lat": 56.1754306, "lon": 10.1406921 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494447746, + "bounds": { + "minlat": 56.1470500, + "minlon": 10.1962304, + "maxlat": 56.1470722, + "maxlon": 10.1963184 + }, + "geometry": [ + { "lat": 56.1470524, "lon": 10.1962304 }, + { "lat": 56.1470722, "lon": 10.1962321 }, + { "lat": 56.1470698, "lon": 10.1963184 }, + { "lat": 56.1470500, "lon": 10.1963167 }, + { "lat": 56.1470524, "lon": 10.1962304 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494556059, + "bounds": { + "minlat": 56.2063627, + "minlon": 10.1736085, + "maxlat": 56.2064212, + "maxlon": 10.1737050 + }, + "geometry": [ + { "lat": 56.2064056, "lon": 10.1736085 }, + { "lat": 56.2063627, "lon": 10.1736442 }, + { "lat": 56.2063784, "lon": 10.1737050 }, + { "lat": 56.2064212, "lon": 10.1736694 }, + { "lat": 56.2064056, "lon": 10.1736085 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494556060, + "bounds": { + "minlat": 56.2065133, + "minlon": 10.1734897, + "maxlat": 56.2065707, + "maxlon": 10.1735871 + }, + "geometry": [ + { "lat": 56.2065552, "lon": 10.1734897 }, + { "lat": 56.2065133, "lon": 10.1735221 }, + { "lat": 56.2065288, "lon": 10.1735871 }, + { "lat": 56.2065707, "lon": 10.1735547 }, + { "lat": 56.2065552, "lon": 10.1734897 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494556061, + "bounds": { + "minlat": 56.2062795, + "minlon": 10.1737275, + "maxlat": 56.2063313, + "maxlon": 10.1738149 + }, + "geometry": [ + { "lat": 56.2063174, "lon": 10.1737275 }, + { "lat": 56.2063313, "lon": 10.1737855 }, + { "lat": 56.2062934, "lon": 10.1738149 }, + { "lat": 56.2062795, "lon": 10.1737568 }, + { "lat": 56.2063174, "lon": 10.1737275 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494612665, + "bounds": { + "minlat": 56.1580363, + "minlon": 10.1948122, + "maxlat": 56.1581019, + "maxlon": 10.1948804 + }, + "geometry": [ + { "lat": 56.1580363, "lon": 10.1948498 }, + { "lat": 56.1580423, "lon": 10.1948804 }, + { "lat": 56.1581019, "lon": 10.1948429 }, + { "lat": 56.1580959, "lon": 10.1948122 }, + { "lat": 56.1580363, "lon": 10.1948498 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494613300, + "bounds": { + "minlat": 56.1564297, + "minlon": 10.1999781, + "maxlat": 56.1564707, + "maxlon": 10.2000355 + }, + "geometry": [ + { "lat": 56.1564707, "lon": 10.2000296 }, + { "lat": 56.1564692, "lon": 10.1999781 }, + { "lat": 56.1564297, "lon": 10.1999846 }, + { "lat": 56.1564321, "lon": 10.2000355 }, + { "lat": 56.1564707, "lon": 10.2000296 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494613733, + "bounds": { + "minlat": 56.1588018, + "minlon": 10.2039142, + "maxlat": 56.1588291, + "maxlon": 10.2039785 + }, + "geometry": [ + { "lat": 56.1588157, "lon": 10.2039785 }, + { "lat": 56.1588291, "lon": 10.2039672 }, + { "lat": 56.1588153, "lon": 10.2039142 }, + { "lat": 56.1588018, "lon": 10.2039254 }, + { "lat": 56.1588157, "lon": 10.2039785 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494613844, + "bounds": { + "minlat": 56.1593656, + "minlon": 10.2042551, + "maxlat": 56.1594134, + "maxlon": 10.2043064 + }, + "geometry": [ + { "lat": 56.1594113, "lon": 10.2042551 }, + { "lat": 56.1593656, "lon": 10.2042622 }, + { "lat": 56.1593677, "lon": 10.2043064 }, + { "lat": 56.1594134, "lon": 10.2042994 }, + { "lat": 56.1594113, "lon": 10.2042551 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494614408, + "bounds": { + "minlat": 56.1628774, + "minlon": 10.2011233, + "maxlat": 56.1629213, + "maxlon": 10.2011913 + }, + "geometry": [ + { "lat": 56.1628774, "lon": 10.2011563 }, + { "lat": 56.1628882, "lon": 10.2011913 }, + { "lat": 56.1629213, "lon": 10.2011582 }, + { "lat": 56.1629105, "lon": 10.2011233 }, + { "lat": 56.1628774, "lon": 10.2011563 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494614587, + "bounds": { + "minlat": 56.1631039, + "minlon": 10.2018585, + "maxlat": 56.1631516, + "maxlon": 10.2019366 + }, + "geometry": [ + { "lat": 56.1631039, "lon": 10.2018923 }, + { "lat": 56.1631380, "lon": 10.2018585 }, + { "lat": 56.1631516, "lon": 10.2019028 }, + { "lat": 56.1631175, "lon": 10.2019366 }, + { "lat": 56.1631039, "lon": 10.2018923 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494666997, + "bounds": { + "minlat": 56.1427198, + "minlon": 10.2030571, + "maxlat": 56.1427597, + "maxlon": 10.2031268 + }, + "geometry": [ + { "lat": 56.1427198, "lon": 10.2030852 }, + { "lat": 56.1427258, "lon": 10.2031268 }, + { "lat": 56.1427597, "lon": 10.2030982 }, + { "lat": 56.1427529, "lon": 10.2030571 }, + { "lat": 56.1427198, "lon": 10.2030852 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494669499, + "bounds": { + "minlat": 56.1426376, + "minlon": 10.2024777, + "maxlat": 56.1426871, + "maxlon": 10.2025998 + }, + "geometry": [ + { "lat": 56.1426813, "lon": 10.2025157 }, + { "lat": 56.1426871, "lon": 10.2025546 }, + { "lat": 56.1426500, "lon": 10.2025998 }, + { "lat": 56.1426437, "lon": 10.2025607 }, + { "lat": 56.1426376, "lon": 10.2025224 }, + { "lat": 56.1426746, "lon": 10.2024777 }, + { "lat": 56.1426813, "lon": 10.2025157 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494669500, + "bounds": { + "minlat": 56.1426437, + "minlon": 10.2025157, + "maxlat": 56.1426871, + "maxlon": 10.2025998 + }, + "geometry": [ + { "lat": 56.1426871, "lon": 10.2025546 }, + { "lat": 56.1426500, "lon": 10.2025998 }, + { "lat": 56.1426437, "lon": 10.2025607 }, + { "lat": 56.1426813, "lon": 10.2025157 }, + { "lat": 56.1426871, "lon": 10.2025546 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494669501, + "bounds": { + "minlat": 56.1426376, + "minlon": 10.2024777, + "maxlat": 56.1426813, + "maxlon": 10.2025607 + }, + "geometry": [ + { "lat": 56.1426376, "lon": 10.2025224 }, + { "lat": 56.1426437, "lon": 10.2025607 }, + { "lat": 56.1426813, "lon": 10.2025157 }, + { "lat": 56.1426746, "lon": 10.2024777 }, + { "lat": 56.1426376, "lon": 10.2025224 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494669847, + "bounds": { + "minlat": 56.1422751, + "minlon": 10.2026184, + "maxlat": 56.1423202, + "maxlon": 10.2027012 + }, + "geometry": [ + { "lat": 56.1422998, "lon": 10.2026184 }, + { "lat": 56.1422751, "lon": 10.2026489 }, + { "lat": 56.1422950, "lon": 10.2027012 }, + { "lat": 56.1423202, "lon": 10.2026708 }, + { "lat": 56.1422998, "lon": 10.2026184 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494670036, + "bounds": { + "minlat": 56.1421402, + "minlon": 10.2027765, + "maxlat": 56.1421857, + "maxlon": 10.2028609 + }, + "geometry": [ + { "lat": 56.1421653, "lon": 10.2027765 }, + { "lat": 56.1421402, "lon": 10.2028071 }, + { "lat": 56.1421606, "lon": 10.2028609 }, + { "lat": 56.1421857, "lon": 10.2028302 }, + { "lat": 56.1421653, "lon": 10.2027765 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494670862, + "bounds": { + "minlat": 56.1442589, + "minlon": 10.2028088, + "maxlat": 56.1442848, + "maxlon": 10.2029116 + }, + "geometry": [ + { "lat": 56.1442777, "lon": 10.2028088 }, + { "lat": 56.1442589, "lon": 10.2028131 }, + { "lat": 56.1442660, "lon": 10.2029116 }, + { "lat": 56.1442848, "lon": 10.2029073 }, + { "lat": 56.1442777, "lon": 10.2028088 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494671758, + "bounds": { + "minlat": 56.1434750, + "minlon": 10.2015399, + "maxlat": 56.1435033, + "maxlon": 10.2016336 + }, + "geometry": [ + { "lat": 56.1434919, "lon": 10.2015399 }, + { "lat": 56.1434750, "lon": 10.2015490 }, + { "lat": 56.1434872, "lon": 10.2016336 }, + { "lat": 56.1435033, "lon": 10.2016264 }, + { "lat": 56.1434919, "lon": 10.2015399 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494672034, + "bounds": { + "minlat": 56.1400263, + "minlon": 10.2001791, + "maxlat": 56.1400594, + "maxlon": 10.2002762 + }, + "geometry": [ + { "lat": 56.1400427, "lon": 10.2001791 }, + { "lat": 56.1400263, "lon": 10.2001893 }, + { "lat": 56.1400431, "lon": 10.2002762 }, + { "lat": 56.1400594, "lon": 10.2002661 }, + { "lat": 56.1400427, "lon": 10.2001791 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494672826, + "bounds": { + "minlat": 56.1409871, + "minlon": 10.1982319, + "maxlat": 56.1410497, + "maxlon": 10.1986846 + }, + "geometry": [ + { "lat": 56.1410497, "lon": 10.1986788 }, + { "lat": 56.1410317, "lon": 10.1986846 }, + { "lat": 56.1409871, "lon": 10.1982377 }, + { "lat": 56.1410050, "lon": 10.1982319 }, + { "lat": 56.1410497, "lon": 10.1986788 } + ], + "tags": { + "amenity": "parking", + "capacity": "4", + "capacity:disabled": "4", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494703397, + "bounds": { + "minlat": 56.1459131, + "minlon": 10.1912035, + "maxlat": 56.1459590, + "maxlon": 10.1912826 + }, + "geometry": [ + { "lat": 56.1459450, "lon": 10.1912035 }, + { "lat": 56.1459131, "lon": 10.1912544 }, + { "lat": 56.1459270, "lon": 10.1912826 }, + { "lat": 56.1459590, "lon": 10.1912316 }, + { "lat": 56.1459450, "lon": 10.1912035 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494703492, + "bounds": { + "minlat": 56.1452078, + "minlon": 10.1922672, + "maxlat": 56.1452433, + "maxlon": 10.1923293 + }, + "geometry": [ + { "lat": 56.1452326, "lon": 10.1922672 }, + { "lat": 56.1452433, "lon": 10.1922878 }, + { "lat": 56.1452184, "lon": 10.1923293 }, + { "lat": 56.1452078, "lon": 10.1923086 }, + { "lat": 56.1452326, "lon": 10.1922672 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494704514, + "bounds": { + "minlat": 56.1432939, + "minlon": 10.1927973, + "maxlat": 56.1433681, + "maxlon": 10.1928686 + }, + "geometry": [ + { "lat": 56.1433681, "lon": 10.1927973 }, + { "lat": 56.1433301, "lon": 10.1928100 }, + { "lat": 56.1432939, "lon": 10.1928686 }, + { "lat": 56.1433323, "lon": 10.1928655 }, + { "lat": 56.1433681, "lon": 10.1927973 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494704664, + "bounds": { + "minlat": 56.1432280, + "minlon": 10.1929398, + "maxlat": 56.1432535, + "maxlon": 10.1930477 + }, + "geometry": [ + { "lat": 56.1432448, "lon": 10.1929398 }, + { "lat": 56.1432280, "lon": 10.1929444 }, + { "lat": 56.1432367, "lon": 10.1930477 }, + { "lat": 56.1432535, "lon": 10.1930432 }, + { "lat": 56.1432448, "lon": 10.1929398 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494704787, + "bounds": { + "minlat": 56.1431682, + "minlon": 10.1921945, + "maxlat": 56.1431921, + "maxlon": 10.1922798 + }, + "geometry": [ + { "lat": 56.1431921, "lon": 10.1922754 }, + { "lat": 56.1431745, "lon": 10.1922798 }, + { "lat": 56.1431682, "lon": 10.1921989 }, + { "lat": 56.1431857, "lon": 10.1921945 }, + { "lat": 56.1431921, "lon": 10.1922754 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494704993, + "bounds": { + "minlat": 56.1431065, + "minlon": 10.1940122, + "maxlat": 56.1431617, + "maxlon": 10.1941063 + }, + "geometry": [ + { "lat": 56.1431617, "lon": 10.1940122 }, + { "lat": 56.1431258, "lon": 10.1940254 }, + { "lat": 56.1431065, "lon": 10.1941063 }, + { "lat": 56.1431391, "lon": 10.1940981 }, + { "lat": 56.1431617, "lon": 10.1940122 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494705273, + "bounds": { + "minlat": 56.1438241, + "minlon": 10.1940108, + "maxlat": 56.1438734, + "maxlon": 10.1940579 + }, + "geometry": [ + { "lat": 56.1438263, "lon": 10.1940579 }, + { "lat": 56.1438241, "lon": 10.1940198 }, + { "lat": 56.1438711, "lon": 10.1940108 }, + { "lat": 56.1438734, "lon": 10.1940489 }, + { "lat": 56.1438263, "lon": 10.1940579 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494705566, + "bounds": { + "minlat": 56.1446167, + "minlon": 10.1936630, + "maxlat": 56.1446513, + "maxlon": 10.1938654 + }, + "geometry": [ + { "lat": 56.1446513, "lon": 10.1938605 }, + { "lat": 56.1446330, "lon": 10.1938654 }, + { "lat": 56.1446248, "lon": 10.1937654 }, + { "lat": 56.1446167, "lon": 10.1936678 }, + { "lat": 56.1446350, "lon": 10.1936630 }, + { "lat": 56.1446430, "lon": 10.1937598 }, + { "lat": 56.1446513, "lon": 10.1938605 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494705567, + "bounds": { + "minlat": 56.1446248, + "minlon": 10.1937598, + "maxlat": 56.1446513, + "maxlon": 10.1938654 + }, + "geometry": [ + { "lat": 56.1446330, "lon": 10.1938654 }, + { "lat": 56.1446513, "lon": 10.1938605 }, + { "lat": 56.1446430, "lon": 10.1937598 }, + { "lat": 56.1446248, "lon": 10.1937654 }, + { "lat": 56.1446330, "lon": 10.1938654 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494705568, + "bounds": { + "minlat": 56.1446167, + "minlon": 10.1936630, + "maxlat": 56.1446430, + "maxlon": 10.1937654 + }, + "geometry": [ + { "lat": 56.1446430, "lon": 10.1937598 }, + { "lat": 56.1446350, "lon": 10.1936630 }, + { "lat": 56.1446167, "lon": 10.1936678 }, + { "lat": 56.1446248, "lon": 10.1937654 }, + { "lat": 56.1446430, "lon": 10.1937598 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494912225, + "bounds": { + "minlat": 56.1583668, + "minlon": 10.1891043, + "maxlat": 56.1584004, + "maxlon": 10.1891865 + }, + "geometry": [ + { "lat": 56.1583864, "lon": 10.1891865 }, + { "lat": 56.1584004, "lon": 10.1891159 }, + { "lat": 56.1583802, "lon": 10.1891043 }, + { "lat": 56.1583668, "lon": 10.1891749 }, + { "lat": 56.1583864, "lon": 10.1891865 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494912226, + "bounds": { + "minlat": 56.1585596, + "minlon": 10.1892569, + "maxlat": 56.1585960, + "maxlon": 10.1893378 + }, + "geometry": [ + { "lat": 56.1585765, "lon": 10.1892569 }, + { "lat": 56.1585960, "lon": 10.1892733 }, + { "lat": 56.1585790, "lon": 10.1893378 }, + { "lat": 56.1585596, "lon": 10.1893214 }, + { "lat": 56.1585765, "lon": 10.1892569 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494912227, + "bounds": { + "minlat": 56.1587045, + "minlon": 10.1893941, + "maxlat": 56.1587460, + "maxlon": 10.1894861 + }, + "geometry": [ + { "lat": 56.1587255, "lon": 10.1893941 }, + { "lat": 56.1587460, "lon": 10.1894131 }, + { "lat": 56.1587250, "lon": 10.1894861 }, + { "lat": 56.1587045, "lon": 10.1894671 }, + { "lat": 56.1587255, "lon": 10.1893941 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494923839, + "bounds": { + "minlat": 56.1575208, + "minlon": 10.1890183, + "maxlat": 56.1576282, + "maxlon": 10.1890591 + }, + "geometry": [ + { "lat": 56.1575208, "lon": 10.1890547 }, + { "lat": 56.1575212, "lon": 10.1890183 }, + { "lat": 56.1575770, "lon": 10.1890199 }, + { "lat": 56.1576282, "lon": 10.1890228 }, + { "lat": 56.1576278, "lon": 10.1890591 }, + { "lat": 56.1575767, "lon": 10.1890569 }, + { "lat": 56.1575208, "lon": 10.1890547 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494923840, + "bounds": { + "minlat": 56.1575208, + "minlon": 10.1890183, + "maxlat": 56.1575770, + "maxlon": 10.1890569 + }, + "geometry": [ + { "lat": 56.1575208, "lon": 10.1890547 }, + { "lat": 56.1575767, "lon": 10.1890569 }, + { "lat": 56.1575770, "lon": 10.1890199 }, + { "lat": 56.1575212, "lon": 10.1890183 }, + { "lat": 56.1575208, "lon": 10.1890547 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494923841, + "bounds": { + "minlat": 56.1575767, + "minlon": 10.1890199, + "maxlat": 56.1576282, + "maxlon": 10.1890591 + }, + "geometry": [ + { "lat": 56.1576282, "lon": 10.1890228 }, + { "lat": 56.1576278, "lon": 10.1890591 }, + { "lat": 56.1575767, "lon": 10.1890569 }, + { "lat": 56.1575770, "lon": 10.1890199 }, + { "lat": 56.1576282, "lon": 10.1890228 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494924897, + "bounds": { + "minlat": 56.1568110, + "minlon": 10.1888243, + "maxlat": 56.1568339, + "maxlon": 10.1888993 + }, + "geometry": [ + { "lat": 56.1568337, "lon": 10.1888993 }, + { "lat": 56.1568110, "lon": 10.1888991 }, + { "lat": 56.1568112, "lon": 10.1888243 }, + { "lat": 56.1568339, "lon": 10.1888244 }, + { "lat": 56.1568337, "lon": 10.1888993 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494926696, + "bounds": { + "minlat": 56.1568016, + "minlon": 10.1864918, + "maxlat": 56.1568585, + "maxlon": 10.1865468 + }, + "geometry": [ + { "lat": 56.1568016, "lon": 10.1865288 }, + { "lat": 56.1568276, "lon": 10.1865468 }, + { "lat": 56.1568585, "lon": 10.1865048 }, + { "lat": 56.1568290, "lon": 10.1864918 }, + { "lat": 56.1568016, "lon": 10.1865288 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494928518, + "bounds": { + "minlat": 56.1571029, + "minlon": 10.1832613, + "maxlat": 56.1571467, + "maxlon": 10.1833980 + }, + "geometry": [ + { "lat": 56.1571467, "lon": 10.1832613 }, + { "lat": 56.1571466, "lon": 10.1833212 }, + { "lat": 56.1571465, "lon": 10.1833811 }, + { "lat": 56.1571029, "lon": 10.1833980 }, + { "lat": 56.1571033, "lon": 10.1833390 }, + { "lat": 56.1571037, "lon": 10.1832792 }, + { "lat": 56.1571467, "lon": 10.1832613 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494928519, + "bounds": { + "minlat": 56.1571029, + "minlon": 10.1833212, + "maxlat": 56.1571466, + "maxlon": 10.1833980 + }, + "geometry": [ + { "lat": 56.1571029, "lon": 10.1833980 }, + { "lat": 56.1571465, "lon": 10.1833811 }, + { "lat": 56.1571466, "lon": 10.1833212 }, + { "lat": 56.1571033, "lon": 10.1833390 }, + { "lat": 56.1571029, "lon": 10.1833980 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494928520, + "bounds": { + "minlat": 56.1571033, + "minlon": 10.1832613, + "maxlat": 56.1571467, + "maxlon": 10.1833390 + }, + "geometry": [ + { "lat": 56.1571466, "lon": 10.1833212 }, + { "lat": 56.1571467, "lon": 10.1832613 }, + { "lat": 56.1571037, "lon": 10.1832792 }, + { "lat": 56.1571033, "lon": 10.1833390 }, + { "lat": 56.1571466, "lon": 10.1833212 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494931265, + "bounds": { + "minlat": 56.1594780, + "minlon": 10.1844293, + "maxlat": 56.1595764, + "maxlon": 10.1844640 + }, + "geometry": [ + { "lat": 56.1595764, "lon": 10.1844328 }, + { "lat": 56.1595289, "lon": 10.1844311 }, + { "lat": 56.1594783, "lon": 10.1844293 }, + { "lat": 56.1594780, "lon": 10.1844604 }, + { "lat": 56.1595283, "lon": 10.1844622 }, + { "lat": 56.1595760, "lon": 10.1844640 }, + { "lat": 56.1595764, "lon": 10.1844328 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494931266, + "bounds": { + "minlat": 56.1595283, + "minlon": 10.1844311, + "maxlat": 56.1595764, + "maxlon": 10.1844640 + }, + "geometry": [ + { "lat": 56.1595760, "lon": 10.1844640 }, + { "lat": 56.1595283, "lon": 10.1844622 }, + { "lat": 56.1595289, "lon": 10.1844311 }, + { "lat": 56.1595764, "lon": 10.1844328 }, + { "lat": 56.1595760, "lon": 10.1844640 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494931267, + "bounds": { + "minlat": 56.1594780, + "minlon": 10.1844293, + "maxlat": 56.1595289, + "maxlon": 10.1844622 + }, + "geometry": [ + { "lat": 56.1595283, "lon": 10.1844622 }, + { "lat": 56.1594780, "lon": 10.1844604 }, + { "lat": 56.1594783, "lon": 10.1844293 }, + { "lat": 56.1595289, "lon": 10.1844311 }, + { "lat": 56.1595283, "lon": 10.1844622 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494931268, + "bounds": { + "minlat": 56.1597368, + "minlon": 10.1844300, + "maxlat": 56.1598322, + "maxlon": 10.1844616 + }, + "geometry": [ + { "lat": 56.1598322, "lon": 10.1844302 }, + { "lat": 56.1597854, "lon": 10.1844301 }, + { "lat": 56.1597368, "lon": 10.1844300 }, + { "lat": 56.1597368, "lon": 10.1844615 }, + { "lat": 56.1597856, "lon": 10.1844615 }, + { "lat": 56.1598322, "lon": 10.1844616 }, + { "lat": 56.1598322, "lon": 10.1844302 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494931269, + "bounds": { + "minlat": 56.1597854, + "minlon": 10.1844301, + "maxlat": 56.1598322, + "maxlon": 10.1844616 + }, + "geometry": [ + { "lat": 56.1597856, "lon": 10.1844615 }, + { "lat": 56.1598322, "lon": 10.1844616 }, + { "lat": 56.1598322, "lon": 10.1844302 }, + { "lat": 56.1597854, "lon": 10.1844301 }, + { "lat": 56.1597856, "lon": 10.1844615 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494931270, + "bounds": { + "minlat": 56.1597368, + "minlon": 10.1844300, + "maxlat": 56.1597856, + "maxlon": 10.1844615 + }, + "geometry": [ + { "lat": 56.1597856, "lon": 10.1844615 }, + { "lat": 56.1597368, "lon": 10.1844615 }, + { "lat": 56.1597368, "lon": 10.1844300 }, + { "lat": 56.1597854, "lon": 10.1844301 }, + { "lat": 56.1597856, "lon": 10.1844615 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494931867, + "bounds": { + "minlat": 56.1625283, + "minlon": 10.1861161, + "maxlat": 56.1625930, + "maxlon": 10.1861724 + }, + "geometry": [ + { "lat": 56.1625472, "lon": 10.1861161 }, + { "lat": 56.1625930, "lon": 10.1861169 }, + { "lat": 56.1625749, "lon": 10.1861715 }, + { "lat": 56.1625283, "lon": 10.1861724 }, + { "lat": 56.1625472, "lon": 10.1861161 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494946190, + "bounds": { + "minlat": 56.1521870, + "minlon": 10.1674363, + "maxlat": 56.1522310, + "maxlon": 10.1675620 + }, + "geometry": [ + { "lat": 56.1521870, "lon": 10.1674502 }, + { "lat": 56.1522099, "lon": 10.1674363 }, + { "lat": 56.1522310, "lon": 10.1675480 }, + { "lat": 56.1522080, "lon": 10.1675620 }, + { "lat": 56.1521870, "lon": 10.1674502 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494946537, + "bounds": { + "minlat": 56.1543403, + "minlon": 10.1867819, + "maxlat": 56.1543894, + "maxlon": 10.1868964 + }, + "geometry": [ + { "lat": 56.1543865, "lon": 10.1867819 }, + { "lat": 56.1543403, "lon": 10.1867858 }, + { "lat": 56.1543418, "lon": 10.1868422 }, + { "lat": 56.1543432, "lon": 10.1868964 }, + { "lat": 56.1543894, "lon": 10.1868924 }, + { "lat": 56.1543880, "lon": 10.1868379 }, + { "lat": 56.1543865, "lon": 10.1867819 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1494946538, + "bounds": { + "minlat": 56.1543418, + "minlon": 10.1868379, + "maxlat": 56.1543894, + "maxlon": 10.1868964 + }, + "geometry": [ + { "lat": 56.1543894, "lon": 10.1868924 }, + { "lat": 56.1543880, "lon": 10.1868379 }, + { "lat": 56.1543418, "lon": 10.1868422 }, + { "lat": 56.1543432, "lon": 10.1868964 }, + { "lat": 56.1543894, "lon": 10.1868924 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494946539, + "bounds": { + "minlat": 56.1543403, + "minlon": 10.1867819, + "maxlat": 56.1543880, + "maxlon": 10.1868422 + }, + "geometry": [ + { "lat": 56.1543880, "lon": 10.1868379 }, + { "lat": 56.1543865, "lon": 10.1867819 }, + { "lat": 56.1543403, "lon": 10.1867858 }, + { "lat": 56.1543418, "lon": 10.1868422 }, + { "lat": 56.1543880, "lon": 10.1868379 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494947009, + "bounds": { + "minlat": 56.1545773, + "minlon": 10.1883725, + "maxlat": 56.1546236, + "maxlon": 10.1884311 + }, + "geometry": [ + { "lat": 56.1546220, "lon": 10.1883725 }, + { "lat": 56.1545773, "lon": 10.1883768 }, + { "lat": 56.1545789, "lon": 10.1884311 }, + { "lat": 56.1546236, "lon": 10.1884268 }, + { "lat": 56.1546220, "lon": 10.1883725 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1494984476, + "bounds": { + "minlat": 56.1548598, + "minlon": 10.1954552, + "maxlat": 56.1548841, + "maxlon": 10.1955684 + }, + "geometry": [ + { "lat": 56.1548598, "lon": 10.1954995 }, + { "lat": 56.1548770, "lon": 10.1954552 }, + { "lat": 56.1548841, "lon": 10.1955318 }, + { "lat": 56.1548672, "lon": 10.1955684 }, + { "lat": 56.1548598, "lon": 10.1954995 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495000129, + "bounds": { + "minlat": 56.1503708, + "minlon": 10.1952704, + "maxlat": 56.1504134, + "maxlon": 10.1953230 + }, + "geometry": [ + { "lat": 56.1503755, "lon": 10.1952704 }, + { "lat": 56.1503708, "lon": 10.1953074 }, + { "lat": 56.1504087, "lon": 10.1953230 }, + { "lat": 56.1504134, "lon": 10.1952860 }, + { "lat": 56.1503755, "lon": 10.1952704 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495000288, + "bounds": { + "minlat": 56.1465609, + "minlon": 10.1926610, + "maxlat": 56.1466084, + "maxlon": 10.1927120 + }, + "geometry": [ + { "lat": 56.1465655, "lon": 10.1927120 }, + { "lat": 56.1465609, "lon": 10.1926831 }, + { "lat": 56.1466037, "lon": 10.1926610 }, + { "lat": 56.1466084, "lon": 10.1926899 }, + { "lat": 56.1465655, "lon": 10.1927120 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495034898, + "bounds": { + "minlat": 56.1450926, + "minlon": 10.1951484, + "maxlat": 56.1451505, + "maxlon": 10.1952244 + }, + "geometry": [ + { "lat": 56.1451298, "lon": 10.1951526 }, + { "lat": 56.1450926, "lon": 10.1951484 }, + { "lat": 56.1451129, "lon": 10.1952180 }, + { "lat": 56.1451505, "lon": 10.1952244 }, + { "lat": 56.1451298, "lon": 10.1951526 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "diagonal", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495083871, + "bounds": { + "minlat": 56.1333840, + "minlon": 10.1819919, + "maxlat": 56.1334177, + "maxlon": 10.1820936 + }, + "geometry": [ + { "lat": 56.1333840, "lon": 10.1820018 }, + { "lat": 56.1334018, "lon": 10.1819919 }, + { "lat": 56.1334177, "lon": 10.1820837 }, + { "lat": 56.1333999, "lon": 10.1820936 }, + { "lat": 56.1333840, "lon": 10.1820018 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495091161, + "bounds": { + "minlat": 56.1436861, + "minlon": 10.1852573, + "maxlat": 56.1437302, + "maxlon": 10.1853280 + }, + "geometry": [ + { "lat": 56.1436861, "lon": 10.1853024 }, + { "lat": 56.1436968, "lon": 10.1852573 }, + { "lat": 56.1437302, "lon": 10.1852828 }, + { "lat": 56.1437195, "lon": 10.1853280 }, + { "lat": 56.1436861, "lon": 10.1853024 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495097528, + "bounds": { + "minlat": 56.1408364, + "minlon": 10.1967692, + "maxlat": 56.1408615, + "maxlon": 10.1968598 + }, + "geometry": [ + { "lat": 56.1408532, "lon": 10.1967692 }, + { "lat": 56.1408364, "lon": 10.1967744 }, + { "lat": 56.1408446, "lon": 10.1968598 }, + { "lat": 56.1408615, "lon": 10.1968546 }, + { "lat": 56.1408532, "lon": 10.1967692 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495165425, + "bounds": { + "minlat": 56.1443558, + "minlon": 10.1880023, + "maxlat": 56.1443990, + "maxlon": 10.1880781 + }, + "geometry": [ + { "lat": 56.1443558, "lon": 10.1880294 }, + { "lat": 56.1443699, "lon": 10.1880023 }, + { "lat": 56.1443990, "lon": 10.1880511 }, + { "lat": 56.1443849, "lon": 10.1880781 }, + { "lat": 56.1443558, "lon": 10.1880294 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495165426, + "bounds": { + "minlat": 56.1442528, + "minlon": 10.1880303, + "maxlat": 56.1442960, + "maxlon": 10.1881061 + }, + "geometry": [ + { "lat": 56.1442528, "lon": 10.1880573 }, + { "lat": 56.1442669, "lon": 10.1880303 }, + { "lat": 56.1442960, "lon": 10.1880790 }, + { "lat": 56.1442819, "lon": 10.1881061 }, + { "lat": 56.1442528, "lon": 10.1880573 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495165427, + "bounds": { + "minlat": 56.1441770, + "minlon": 10.1878976, + "maxlat": 56.1442201, + "maxlon": 10.1879734 + }, + "geometry": [ + { "lat": 56.1441770, "lon": 10.1879247 }, + { "lat": 56.1441911, "lon": 10.1878976 }, + { "lat": 56.1442201, "lon": 10.1879464 }, + { "lat": 56.1442061, "lon": 10.1879734 }, + { "lat": 56.1441770, "lon": 10.1879247 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495182700, + "bounds": { + "minlat": 56.1388040, + "minlon": 10.1895595, + "maxlat": 56.1388294, + "maxlon": 10.1896508 + }, + "geometry": [ + { "lat": 56.1388294, "lon": 10.1896481 }, + { "lat": 56.1388072, "lon": 10.1896508 }, + { "lat": 56.1388040, "lon": 10.1895621 }, + { "lat": 56.1388261, "lon": 10.1895595 }, + { "lat": 56.1388294, "lon": 10.1896481 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495183128, + "bounds": { + "minlat": 56.1388655, + "minlon": 10.1905230, + "maxlat": 56.1389032, + "maxlon": 10.1907162 + }, + "geometry": [ + { "lat": 56.1389032, "lon": 10.1907102 }, + { "lat": 56.1388865, "lon": 10.1907162 }, + { "lat": 56.1388769, "lon": 10.1906270 }, + { "lat": 56.1388655, "lon": 10.1905293 }, + { "lat": 56.1388822, "lon": 10.1905230 }, + { "lat": 56.1388936, "lon": 10.1906207 }, + { "lat": 56.1389032, "lon": 10.1907102 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1495183129, + "bounds": { + "minlat": 56.1388769, + "minlon": 10.1906207, + "maxlat": 56.1389032, + "maxlon": 10.1907162 + }, + "geometry": [ + { "lat": 56.1388865, "lon": 10.1907162 }, + { "lat": 56.1388769, "lon": 10.1906270 }, + { "lat": 56.1388936, "lon": 10.1906207 }, + { "lat": 56.1389032, "lon": 10.1907102 }, + { "lat": 56.1388865, "lon": 10.1907162 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1495183130, + "bounds": { + "minlat": 56.1388655, + "minlon": 10.1905230, + "maxlat": 56.1388936, + "maxlon": 10.1906270 + }, + "geometry": [ + { "lat": 56.1388936, "lon": 10.1906207 }, + { "lat": 56.1388822, "lon": 10.1905230 }, + { "lat": 56.1388655, "lon": 10.1905293 }, + { "lat": 56.1388769, "lon": 10.1906270 }, + { "lat": 56.1388936, "lon": 10.1906207 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1495970204, + "bounds": { + "minlat": 56.1765467, + "minlon": 10.1361239, + "maxlat": 56.1766237, + "maxlon": 10.1362052 + }, + "geometry": [ + { "lat": 56.1766221, "lon": 10.1362052 }, + { "lat": 56.1765467, "lon": 10.1362001 }, + { "lat": 56.1765483, "lon": 10.1361239 }, + { "lat": 56.1765770, "lon": 10.1361258 }, + { "lat": 56.1766237, "lon": 10.1361290 }, + { "lat": 56.1766221, "lon": 10.1362052 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1495970205, + "bounds": { + "minlat": 56.1765448, + "minlon": 10.1362001, + "maxlat": 56.1766221, + "maxlon": 10.1362722 + }, + "geometry": [ + { "lat": 56.1766212, "lon": 10.1362722 }, + { "lat": 56.1765448, "lon": 10.1362689 }, + { "lat": 56.1765467, "lon": 10.1362001 }, + { "lat": 56.1766221, "lon": 10.1362052 }, + { "lat": 56.1766212, "lon": 10.1362722 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1495970211, + "bounds": { + "minlat": 56.1768972, + "minlon": 10.1370306, + "maxlat": 56.1769529, + "maxlon": 10.1371329 + }, + "geometry": [ + { "lat": 56.1768972, "lon": 10.1371038 }, + { "lat": 56.1769358, "lon": 10.1371329 }, + { "lat": 56.1769529, "lon": 10.1370597 }, + { "lat": 56.1769143, "lon": 10.1370306 }, + { "lat": 56.1768972, "lon": 10.1371038 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1497331546, + "bounds": { + "minlat": 56.1688988, + "minlon": 10.1960691, + "maxlat": 56.1690293, + "maxlon": 10.1963595 + }, + "geometry": [ + { "lat": 56.1689420, "lon": 10.1960691 }, + { "lat": 56.1689601, "lon": 10.1961184 }, + { "lat": 56.1690293, "lon": 10.1963070 }, + { "lat": 56.1689842, "lon": 10.1963595 }, + { "lat": 56.1689160, "lon": 10.1961674 }, + { "lat": 56.1688988, "lon": 10.1961188 }, + { "lat": 56.1689420, "lon": 10.1960691 } + ], + "tags": { + "amenity": "parking", + "capacity": "7", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1497331551, + "bounds": { + "minlat": 56.1693807, + "minlon": 10.1975831, + "maxlat": 56.1695269, + "maxlon": 10.1979758 + }, + "geometry": [ + { "lat": 56.1694150, "lon": 10.1975831 }, + { "lat": 56.1694309, "lon": 10.1976334 }, + { "lat": 56.1695269, "lon": 10.1979367 }, + { "lat": 56.1694890, "lon": 10.1979758 }, + { "lat": 56.1693956, "lon": 10.1976667 }, + { "lat": 56.1693807, "lon": 10.1976174 }, + { "lat": 56.1694150, "lon": 10.1975831 } + ], + "tags": { + "amenity": "parking", + "capacity": "10", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1499739572, + "bounds": { + "minlat": 56.1871764, + "minlon": 10.1975175, + "maxlat": 56.1872506, + "maxlon": 10.1976772 + }, + "geometry": [ + { "lat": 56.1871764, "lon": 10.1976594 }, + { "lat": 56.1871891, "lon": 10.1976772 }, + { "lat": 56.1872202, "lon": 10.1976063 }, + { "lat": 56.1872506, "lon": 10.1975352 }, + { "lat": 56.1872377, "lon": 10.1975175 }, + { "lat": 56.1872074, "lon": 10.1975885 }, + { "lat": 56.1871764, "lon": 10.1976594 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1499739573, + "bounds": { + "minlat": 56.1872074, + "minlon": 10.1975175, + "maxlat": 56.1872506, + "maxlon": 10.1976063 + }, + "geometry": [ + { "lat": 56.1872074, "lon": 10.1975885 }, + { "lat": 56.1872202, "lon": 10.1976063 }, + { "lat": 56.1872506, "lon": 10.1975352 }, + { "lat": 56.1872377, "lon": 10.1975175 }, + { "lat": 56.1872074, "lon": 10.1975885 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1499739574, + "bounds": { + "minlat": 56.1848112, + "minlon": 10.2019064, + "maxlat": 56.1848647, + "maxlon": 10.2020042 + }, + "geometry": [ + { "lat": 56.1848385, "lon": 10.2019064 }, + { "lat": 56.1848112, "lon": 10.2019459 }, + { "lat": 56.1848374, "lon": 10.2020042 }, + { "lat": 56.1848647, "lon": 10.2019647 }, + { "lat": 56.1848385, "lon": 10.2019064 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "yes", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1502250441, + "bounds": { + "minlat": 56.0909424, + "minlon": 10.1360122, + "maxlat": 56.0910491, + "maxlon": 10.1361032 + }, + "geometry": [ + { "lat": 56.0909492, "lon": 10.1360122 }, + { "lat": 56.0909424, "lon": 10.1360486 }, + { "lat": 56.0909910, "lon": 10.1360761 }, + { "lat": 56.0910430, "lon": 10.1361032 }, + { "lat": 56.0910491, "lon": 10.1360653 }, + { "lat": 56.0909972, "lon": 10.1360382 }, + { "lat": 56.0909492, "lon": 10.1360122 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1502250442, + "bounds": { + "minlat": 56.0909910, + "minlon": 10.1360382, + "maxlat": 56.0910491, + "maxlon": 10.1361032 + }, + "geometry": [ + { "lat": 56.0910430, "lon": 10.1361032 }, + { "lat": 56.0909910, "lon": 10.1360761 }, + { "lat": 56.0909972, "lon": 10.1360382 }, + { "lat": 56.0910491, "lon": 10.1360653 }, + { "lat": 56.0910430, "lon": 10.1361032 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502250443, + "bounds": { + "minlat": 56.0909424, + "minlon": 10.1360122, + "maxlat": 56.0909972, + "maxlon": 10.1360761 + }, + "geometry": [ + { "lat": 56.0909910, "lon": 10.1360761 }, + { "lat": 56.0909424, "lon": 10.1360486 }, + { "lat": 56.0909492, "lon": 10.1360122 }, + { "lat": 56.0909972, "lon": 10.1360382 }, + { "lat": 56.0909910, "lon": 10.1360761 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502254306, + "bounds": { + "minlat": 56.0907533, + "minlon": 10.1366913, + "maxlat": 56.0907894, + "maxlon": 10.1368079 + }, + "geometry": [ + { "lat": 56.0907533, "lon": 10.1367432 }, + { "lat": 56.0907629, "lon": 10.1366913 }, + { "lat": 56.0907894, "lon": 10.1367620 }, + { "lat": 56.0907812, "lon": 10.1368079 }, + { "lat": 56.0907533, "lon": 10.1367432 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502254308, + "bounds": { + "minlat": 56.0904741, + "minlon": 10.1378529, + "maxlat": 56.0905252, + "maxlon": 10.1380242 + }, + "geometry": [ + { "lat": 56.0905252, "lon": 10.1378660 }, + { "lat": 56.0905017, "lon": 10.1378529 }, + { "lat": 56.0904875, "lon": 10.1379317 }, + { "lat": 56.0904741, "lon": 10.1380114 }, + { "lat": 56.0904977, "lon": 10.1380242 }, + { "lat": 56.0905111, "lon": 10.1379445 }, + { "lat": 56.0905252, "lon": 10.1378660 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1502254309, + "bounds": { + "minlat": 56.0904875, + "minlon": 10.1378529, + "maxlat": 56.0905252, + "maxlon": 10.1379445 + }, + "geometry": [ + { "lat": 56.0905111, "lon": 10.1379445 }, + { "lat": 56.0904875, "lon": 10.1379317 }, + { "lat": 56.0905017, "lon": 10.1378529 }, + { "lat": 56.0905252, "lon": 10.1378660 }, + { "lat": 56.0905111, "lon": 10.1379445 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502254310, + "bounds": { + "minlat": 56.0904741, + "minlon": 10.1379317, + "maxlat": 56.0905111, + "maxlon": 10.1380242 + }, + "geometry": [ + { "lat": 56.0905111, "lon": 10.1379445 }, + { "lat": 56.0904977, "lon": 10.1380242 }, + { "lat": 56.0904741, "lon": 10.1380114 }, + { "lat": 56.0904875, "lon": 10.1379317 }, + { "lat": 56.0905111, "lon": 10.1379445 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502254313, + "bounds": { + "minlat": 56.0913304, + "minlon": 10.1366883, + "maxlat": 56.0913734, + "maxlon": 10.1367749 + }, + "geometry": [ + { "lat": 56.0913426, "lon": 10.1366883 }, + { "lat": 56.0913734, "lon": 10.1367057 }, + { "lat": 56.0913613, "lon": 10.1367749 }, + { "lat": 56.0913304, "lon": 10.1367574 }, + { "lat": 56.0913426, "lon": 10.1366883 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502255093, + "bounds": { + "minlat": 56.1236558, + "minlon": 10.1520969, + "maxlat": 56.1237184, + "maxlon": 10.1522045 + }, + "geometry": [ + { "lat": 56.1236962, "lon": 10.1520969 }, + { "lat": 56.1236558, "lon": 10.1521484 }, + { "lat": 56.1236780, "lon": 10.1522045 }, + { "lat": 56.1237184, "lon": 10.1521531 }, + { "lat": 56.1236962, "lon": 10.1520969 } + ], + "tags": { + "amenity": "parking_space", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502255094, + "bounds": { + "minlat": 56.1236780, + "minlon": 10.1521531, + "maxlat": 56.1237387, + "maxlon": 10.1522559 + }, + "geometry": [ + { "lat": 56.1237184, "lon": 10.1521531 }, + { "lat": 56.1237387, "lon": 10.1522052 }, + { "lat": 56.1236986, "lon": 10.1522559 }, + { "lat": 56.1236780, "lon": 10.1522045 }, + { "lat": 56.1237184, "lon": 10.1521531 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502255095, + "bounds": { + "minlat": 56.1245924, + "minlon": 10.1539875, + "maxlat": 56.1246449, + "maxlon": 10.1540749 + }, + "geometry": [ + { "lat": 56.1245924, "lon": 10.1540291 }, + { "lat": 56.1246088, "lon": 10.1540749 }, + { "lat": 56.1246449, "lon": 10.1540332 }, + { "lat": 56.1246285, "lon": 10.1539875 }, + { "lat": 56.1245924, "lon": 10.1540291 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502255096, + "bounds": { + "minlat": 56.1246361, + "minlon": 10.1541090, + "maxlat": 56.1246891, + "maxlon": 10.1541972 + }, + "geometry": [ + { "lat": 56.1246361, "lon": 10.1541509 }, + { "lat": 56.1246526, "lon": 10.1541972 }, + { "lat": 56.1246891, "lon": 10.1541554 }, + { "lat": 56.1246726, "lon": 10.1541090 }, + { "lat": 56.1246361, "lon": 10.1541509 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502486422, + "bounds": { + "minlat": 56.1726994, + "minlon": 10.2178553, + "maxlat": 56.1727289, + "maxlon": 10.2179660 + }, + "geometry": [ + { "lat": 56.1727289, "lon": 10.2178617 }, + { "lat": 56.1727113, "lon": 10.2178553 }, + { "lat": 56.1726994, "lon": 10.2179595 }, + { "lat": 56.1727171, "lon": 10.2179660 }, + { "lat": 56.1727289, "lon": 10.2178617 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1502488241, + "bounds": { + "minlat": 56.1728448, + "minlon": 10.2161263, + "maxlat": 56.1728795, + "maxlon": 10.2162213 + }, + "geometry": [ + { "lat": 56.1728795, "lon": 10.2161351 }, + { "lat": 56.1728547, "lon": 10.2161263 }, + { "lat": 56.1728448, "lon": 10.2162125 }, + { "lat": 56.1728703, "lon": 10.2162213 }, + { "lat": 56.1728795, "lon": 10.2161351 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1502490850, + "bounds": { + "minlat": 56.1718453, + "minlon": 10.2152846, + "maxlat": 56.1718734, + "maxlon": 10.2153667 + }, + "geometry": [ + { "lat": 56.1718453, "lon": 10.2153592 }, + { "lat": 56.1718644, "lon": 10.2153667 }, + { "lat": 56.1718734, "lon": 10.2152921 }, + { "lat": 56.1718544, "lon": 10.2152846 }, + { "lat": 56.1718453, "lon": 10.2153592 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1502502635, + "bounds": { + "minlat": 56.1706249, + "minlon": 10.2122800, + "maxlat": 56.1706758, + "maxlon": 10.2124869 + }, + "geometry": [ + { "lat": 56.1706758, "lon": 10.2122898 }, + { "lat": 56.1706572, "lon": 10.2122800 }, + { "lat": 56.1706249, "lon": 10.2124771 }, + { "lat": 56.1706436, "lon": 10.2124869 }, + { "lat": 56.1706758, "lon": 10.2122898 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1502553136, + "bounds": { + "minlat": 56.1853572, + "minlon": 10.2052853, + "maxlat": 56.1854082, + "maxlon": 10.2053941 + }, + "geometry": [ + { "lat": 56.1853572, "lon": 10.2053105 }, + { "lat": 56.1853764, "lon": 10.2052853 }, + { "lat": 56.1854082, "lon": 10.2053664 }, + { "lat": 56.1853893, "lon": 10.2053941 }, + { "lat": 56.1853572, "lon": 10.2053105 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502553659, + "bounds": { + "minlat": 56.1857785, + "minlon": 10.2064491, + "maxlat": 56.1858220, + "maxlon": 10.2065606 + }, + "geometry": [ + { "lat": 56.1857953, "lon": 10.2065606 }, + { "lat": 56.1858220, "lon": 10.2065456 }, + { "lat": 56.1858052, "lon": 10.2064491 }, + { "lat": 56.1857785, "lon": 10.2064640 }, + { "lat": 56.1857953, "lon": 10.2065606 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1502556371, + "bounds": { + "minlat": 56.1782915, + "minlon": 10.1994588, + "maxlat": 56.1783235, + "maxlon": 10.1995404 + }, + "geometry": [ + { "lat": 56.1783235, "lon": 10.1994661 }, + { "lat": 56.1783056, "lon": 10.1994588 }, + { "lat": 56.1782915, "lon": 10.1995308 }, + { "lat": 56.1783100, "lon": 10.1995404 }, + { "lat": 56.1783235, "lon": 10.1994661 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1504203278, + "bounds": { + "minlat": 56.1827569, + "minlon": 10.0995066, + "maxlat": 56.1828074, + "maxlon": 10.0995912 + }, + "geometry": [ + { "lat": 56.1827569, "lon": 10.0995484 }, + { "lat": 56.1827727, "lon": 10.0995066 }, + { "lat": 56.1828074, "lon": 10.0995493 }, + { "lat": 56.1827920, "lon": 10.0995912 }, + { "lat": 56.1827569, "lon": 10.0995484 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1506069063, + "bounds": { + "minlat": 56.1738792, + "minlon": 10.1803770, + "maxlat": 56.1739300, + "maxlon": 10.1804496 + }, + "geometry": [ + { "lat": 56.1739209, "lon": 10.1803770 }, + { "lat": 56.1739300, "lon": 10.1804037 }, + { "lat": 56.1738884, "lon": 10.1804496 }, + { "lat": 56.1738792, "lon": 10.1804229 }, + { "lat": 56.1739209, "lon": 10.1803770 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1506069388, + "bounds": { + "minlat": 56.1729899, + "minlon": 10.1811836, + "maxlat": 56.1730317, + "maxlon": 10.1812724 + }, + "geometry": [ + { "lat": 56.1730083, "lon": 10.1811836 }, + { "lat": 56.1729899, "lon": 10.1812037 }, + { "lat": 56.1730133, "lon": 10.1812724 }, + { "lat": 56.1730317, "lon": 10.1812523 }, + { "lat": 56.1730083, "lon": 10.1811836 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1523410301, + "bounds": { + "minlat": 56.1470725, + "minlon": 10.2108000, + "maxlat": 56.1471264, + "maxlon": 10.2108883 + }, + "geometry": [ + { "lat": 56.1470725, "lon": 10.2108501 }, + { "lat": 56.1471095, "lon": 10.2108883 }, + { "lat": 56.1471264, "lon": 10.2108360 }, + { "lat": 56.1470881, "lon": 10.2108000 }, + { "lat": 56.1470761, "lon": 10.2108394 }, + { "lat": 56.1470725, "lon": 10.2108501 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1523410302, + "bounds": { + "minlat": 56.1470209, + "minlon": 10.2107987, + "maxlat": 56.1470761, + "maxlon": 10.2108907 + }, + "geometry": [ + { "lat": 56.1470725, "lon": 10.2108501 }, + { "lat": 56.1470592, "lon": 10.2108907 }, + { "lat": 56.1470209, "lon": 10.2108500 }, + { "lat": 56.1470377, "lon": 10.2107987 }, + { "lat": 56.1470761, "lon": 10.2108394 }, + { "lat": 56.1470725, "lon": 10.2108501 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1523410303, + "bounds": { + "minlat": 56.1469206, + "minlon": 10.2107221, + "maxlat": 56.1469750, + "maxlon": 10.2108119 + }, + "geometry": [ + { "lat": 56.1469206, "lon": 10.2107724 }, + { "lat": 56.1469367, "lon": 10.2107221 }, + { "lat": 56.1469750, "lon": 10.2107616 }, + { "lat": 56.1469590, "lon": 10.2108119 }, + { "lat": 56.1469206, "lon": 10.2107724 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1523803081, + "bounds": { + "minlat": 56.1712362, + "minlon": 10.2193794, + "maxlat": 56.1712899, + "maxlon": 10.2195306 + }, + "geometry": [ + { "lat": 56.1712362, "lon": 10.2195306 }, + { "lat": 56.1712813, "lon": 10.2194510 }, + { "lat": 56.1712899, "lon": 10.2193794 }, + { "lat": 56.1712437, "lon": 10.2194650 }, + { "lat": 56.1712362, "lon": 10.2195306 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1525993211, + "bounds": { + "minlat": 56.1417975, + "minlon": 10.1944752, + "maxlat": 56.1418487, + "maxlon": 10.1945191 + }, + "geometry": [ + { "lat": 56.1418465, "lon": 10.1944752 }, + { "lat": 56.1418487, "lon": 10.1945090 }, + { "lat": 56.1417997, "lon": 10.1945191 }, + { "lat": 56.1417975, "lon": 10.1944852 }, + { "lat": 56.1418465, "lon": 10.1944752 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1525995579, + "bounds": { + "minlat": 56.1440106, + "minlon": 10.1951041, + "maxlat": 56.1440664, + "maxlon": 10.1951505 + }, + "geometry": [ + { "lat": 56.1440106, "lon": 10.1951151 }, + { "lat": 56.1440128, "lon": 10.1951505 }, + { "lat": 56.1440664, "lon": 10.1951395 }, + { "lat": 56.1440642, "lon": 10.1951041 }, + { "lat": 56.1440106, "lon": 10.1951151 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1525996955, + "bounds": { + "minlat": 56.1456263, + "minlon": 10.1873179, + "maxlat": 56.1456678, + "maxlon": 10.1873896 + }, + "geometry": [ + { "lat": 56.1456678, "lon": 10.1873527 }, + { "lat": 56.1456413, "lon": 10.1873896 }, + { "lat": 56.1456263, "lon": 10.1873548 }, + { "lat": 56.1456528, "lon": 10.1873179 }, + { "lat": 56.1456678, "lon": 10.1873527 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1525997869, + "bounds": { + "minlat": 56.1433434, + "minlon": 10.1872179, + "maxlat": 56.1433978, + "maxlon": 10.1873152 + }, + "geometry": [ + { "lat": 56.1433434, "lon": 10.1872508 }, + { "lat": 56.1433798, "lon": 10.1873152 }, + { "lat": 56.1433978, "lon": 10.1872818 }, + { "lat": 56.1433612, "lon": 10.1872179 }, + { "lat": 56.1433434, "lon": 10.1872508 } + ], + "tags": { + "access": "yes", + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1525998781, + "bounds": { + "minlat": 56.1438873, + "minlon": 10.1844113, + "maxlat": 56.1439345, + "maxlon": 10.1845426 + }, + "geometry": [ + { "lat": 56.1439265, "lon": 10.1844113 }, + { "lat": 56.1438873, "lon": 10.1844195 }, + { "lat": 56.1438953, "lon": 10.1845426 }, + { "lat": 56.1439345, "lon": 10.1845344 }, + { "lat": 56.1439313, "lon": 10.1844861 }, + { "lat": 56.1439265, "lon": 10.1844113 } + ], + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1525999087, + "bounds": { + "minlat": 56.1450906, + "minlon": 10.1841764, + "maxlat": 56.1451420, + "maxlon": 10.1842353 + }, + "geometry": [ + { "lat": 56.1450939, "lon": 10.1842353 }, + { "lat": 56.1451420, "lon": 10.1842238 }, + { "lat": 56.1451390, "lon": 10.1841764 }, + { "lat": 56.1450906, "lon": 10.1841879 }, + { "lat": 56.1450939, "lon": 10.1842353 } + ], + "tags": { + "access": "yes", + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1527761608, + "bounds": { + "minlat": 56.1260369, + "minlon": 10.1572757, + "maxlat": 56.1260956, + "maxlon": 10.1573780 + }, + "geometry": [ + { "lat": 56.1260369, "lon": 10.1573302 }, + { "lat": 56.1260601, "lon": 10.1573780 }, + { "lat": 56.1260956, "lon": 10.1573232 }, + { "lat": 56.1260740, "lon": 10.1572757 }, + { "lat": 56.1260369, "lon": 10.1573302 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1527761609, + "bounds": { + "minlat": 56.1260601, + "minlon": 10.1573232, + "maxlat": 56.1261218, + "maxlon": 10.1574326 + }, + "geometry": [ + { "lat": 56.1260601, "lon": 10.1573780 }, + { "lat": 56.1260862, "lon": 10.1574326 }, + { "lat": 56.1261218, "lon": 10.1573778 }, + { "lat": 56.1260956, "lon": 10.1573232 }, + { "lat": 56.1260601, "lon": 10.1573780 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1528708670, + "bounds": { + "minlat": 56.1124611, + "minlon": 10.1923621, + "maxlat": 56.1125027, + "maxlon": 10.1924172 + }, + "geometry": [ + { "lat": 56.1124611, "lon": 10.1924172 }, + { "lat": 56.1125022, "lon": 10.1924165 }, + { "lat": 56.1125027, "lon": 10.1923628 }, + { "lat": 56.1124614, "lon": 10.1923621 }, + { "lat": 56.1124611, "lon": 10.1924172 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1528708671, + "bounds": { + "minlat": 56.1124614, + "minlon": 10.1923043, + "maxlat": 56.1125027, + "maxlon": 10.1923628 + }, + "geometry": [ + { "lat": 56.1125024, "lon": 10.1923043 }, + { "lat": 56.1125027, "lon": 10.1923628 }, + { "lat": 56.1124614, "lon": 10.1923621 }, + { "lat": 56.1124617, "lon": 10.1923043 }, + { "lat": 56.1125024, "lon": 10.1923043 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1528708672, + "bounds": { + "minlat": 56.1124617, + "minlon": 10.1922468, + "maxlat": 56.1125027, + "maxlon": 10.1923043 + }, + "geometry": [ + { "lat": 56.1125024, "lon": 10.1923043 }, + { "lat": 56.1125027, "lon": 10.1922482 }, + { "lat": 56.1124620, "lon": 10.1922468 }, + { "lat": 56.1124617, "lon": 10.1923043 }, + { "lat": 56.1125024, "lon": 10.1923043 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1528708673, + "bounds": { + "minlat": 56.1125675, + "minlon": 10.1921876, + "maxlat": 56.1126584, + "maxlon": 10.1922655 + }, + "geometry": [ + { "lat": 56.1125675, "lon": 10.1922642 }, + { "lat": 56.1126584, "lon": 10.1922655 }, + { "lat": 56.1126576, "lon": 10.1921896 }, + { "lat": 56.1125675, "lon": 10.1921876 }, + { "lat": 56.1125675, "lon": 10.1922642 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1528708674, + "bounds": { + "minlat": 56.1125675, + "minlon": 10.1921124, + "maxlat": 56.1126580, + "maxlon": 10.1921896 + }, + "geometry": [ + { "lat": 56.1126580, "lon": 10.1921143 }, + { "lat": 56.1125675, "lon": 10.1921124 }, + { "lat": 56.1125675, "lon": 10.1921876 }, + { "lat": 56.1126576, "lon": 10.1921896 }, + { "lat": 56.1126580, "lon": 10.1921143 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1530316999, + "bounds": { + "minlat": 56.1603473, + "minlon": 10.1925981, + "maxlat": 56.1603993, + "maxlon": 10.1927528 + }, + "geometry": [ + { "lat": 56.1603993, "lon": 10.1926241 }, + { "lat": 56.1603621, "lon": 10.1925981 }, + { "lat": 56.1603500, "lon": 10.1926502 }, + { "lat": 56.1603473, "lon": 10.1927050 }, + { "lat": 56.1603551, "lon": 10.1927528 }, + { "lat": 56.1603962, "lon": 10.1927317 }, + { "lat": 56.1603993, "lon": 10.1926241 } + ], + "tags": { + "amenity": "parking", + "capacity": "3", + "capacity:disabled": "3", + "disabled": "designated", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1530317000, + "bounds": { + "minlat": 56.1604702, + "minlon": 10.1925398, + "maxlat": 56.1605309, + "maxlon": 10.1926537 + }, + "geometry": [ + { "lat": 56.1604796, "lon": 10.1925398 }, + { "lat": 56.1605141, "lon": 10.1925784 }, + { "lat": 56.1605309, "lon": 10.1926256 }, + { "lat": 56.1604980, "lon": 10.1926537 }, + { "lat": 56.1604702, "lon": 10.1926178 }, + { "lat": 56.1604796, "lon": 10.1925398 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "disabled": "designated", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1530317020, + "bounds": { + "minlat": 56.1610112, + "minlon": 10.1975584, + "maxlat": 56.1610468, + "maxlon": 10.1976492 + }, + "geometry": [ + { "lat": 56.1610317, "lon": 10.1975584 }, + { "lat": 56.1610112, "lon": 10.1975712 }, + { "lat": 56.1610263, "lon": 10.1976492 }, + { "lat": 56.1610468, "lon": 10.1976364 }, + { "lat": 56.1610317, "lon": 10.1975584 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1530317721, + "bounds": { + "minlat": 56.1589914, + "minlon": 10.1985464, + "maxlat": 56.1590359, + "maxlon": 10.1986240 + }, + "geometry": [ + { "lat": 56.1589914, "lon": 10.1986046 }, + { "lat": 56.1590016, "lon": 10.1986240 }, + { "lat": 56.1590359, "lon": 10.1985657 }, + { "lat": 56.1590257, "lon": 10.1985464 }, + { "lat": 56.1589914, "lon": 10.1986046 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1530317722, + "bounds": { + "minlat": 56.1588890, + "minlon": 10.1987225, + "maxlat": 56.1589334, + "maxlon": 10.1988017 + }, + "geometry": [ + { "lat": 56.1588890, "lon": 10.1987797 }, + { "lat": 56.1589011, "lon": 10.1988017 }, + { "lat": 56.1589334, "lon": 10.1987444 }, + { "lat": 56.1589213, "lon": 10.1987225 }, + { "lat": 56.1588890, "lon": 10.1987797 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1530318672, + "bounds": { + "minlat": 56.1550827, + "minlon": 10.1986680, + "maxlat": 56.1551370, + "maxlon": 10.1987652 + }, + "geometry": [ + { "lat": 56.1550827, "lon": 10.1986948 }, + { "lat": 56.1550975, "lon": 10.1986680 }, + { "lat": 56.1551370, "lon": 10.1987383 }, + { "lat": 56.1551221, "lon": 10.1987652 }, + { "lat": 56.1550827, "lon": 10.1986948 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1530318834, + "bounds": { + "minlat": 56.1585566, + "minlon": 10.2029481, + "maxlat": 56.1585899, + "maxlon": 10.2030290 + }, + "geometry": [ + { "lat": 56.1585722, "lon": 10.2030290 }, + { "lat": 56.1585899, "lon": 10.2030158 }, + { "lat": 56.1585743, "lon": 10.2029481 }, + { "lat": 56.1585566, "lon": 10.2029613 }, + { "lat": 56.1585722, "lon": 10.2030290 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1530766467, + "bounds": { + "minlat": 56.1032549, + "minlon": 10.1445282, + "maxlat": 56.1033050, + "maxlon": 10.1446193 + }, + "geometry": [ + { "lat": 56.1032785, "lon": 10.1446193 }, + { "lat": 56.1033050, "lon": 10.1445660 }, + { "lat": 56.1032814, "lon": 10.1445282 }, + { "lat": 56.1032549, "lon": 10.1445815 }, + { "lat": 56.1032785, "lon": 10.1446193 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1530766468, + "bounds": { + "minlat": 56.1032785, + "minlon": 10.1445660, + "maxlat": 56.1033290, + "maxlon": 10.1446575 + }, + "geometry": [ + { "lat": 56.1033050, "lon": 10.1445660 }, + { "lat": 56.1033290, "lon": 10.1446045 }, + { "lat": 56.1033020, "lon": 10.1446575 }, + { "lat": 56.1032785, "lon": 10.1446193 }, + { "lat": 56.1033050, "lon": 10.1445660 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1531182091, + "bounds": { + "minlat": 56.1151591, + "minlon": 10.1343220, + "maxlat": 56.1152144, + "maxlon": 10.1344153 + }, + "geometry": [ + { "lat": 56.1152144, "lon": 10.1343825 }, + { "lat": 56.1152033, "lon": 10.1343220 }, + { "lat": 56.1151591, "lon": 10.1343490 }, + { "lat": 56.1151731, "lon": 10.1344153 }, + { "lat": 56.1152144, "lon": 10.1343825 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1535142351, + "bounds": { + "minlat": 56.1652074, + "minlon": 10.2263777, + "maxlat": 56.1652807, + "maxlon": 10.2265021 + }, + "geometry": [ + { "lat": 56.1652074, "lon": 10.2264245 }, + { "lat": 56.1652587, "lon": 10.2265021 }, + { "lat": 56.1652807, "lon": 10.2264552 }, + { "lat": 56.1652293, "lon": 10.2263777 }, + { "lat": 56.1652074, "lon": 10.2264245 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "fee": "yes", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1535142352, + "bounds": { + "minlat": 56.1652293, + "minlon": 10.2263291, + "maxlat": 56.1653031, + "maxlon": 10.2264552 + }, + "geometry": [ + { "lat": 56.1652293, "lon": 10.2263777 }, + { "lat": 56.1652807, "lon": 10.2264552 }, + { "lat": 56.1653031, "lon": 10.2264078 }, + { "lat": 56.1652855, "lon": 10.2263809 }, + { "lat": 56.1652519, "lon": 10.2263291 }, + { "lat": 56.1652293, "lon": 10.2263777 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "fee": "yes", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1535146951, + "bounds": { + "minlat": 56.1866923, + "minlon": 10.1878833, + "maxlat": 56.1867486, + "maxlon": 10.1880498 + }, + "geometry": [ + { "lat": 56.1866923, "lon": 10.1878991 }, + { "lat": 56.1867280, "lon": 10.1880498 }, + { "lat": 56.1867486, "lon": 10.1880340 }, + { "lat": 56.1867129, "lon": 10.1878833 }, + { "lat": 56.1866923, "lon": 10.1878991 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1535146952, + "bounds": { + "minlat": 56.1866417, + "minlon": 10.1876743, + "maxlat": 56.1866916, + "maxlon": 10.1878061 + }, + "geometry": [ + { "lat": 56.1866417, "lon": 10.1876915 }, + { "lat": 56.1866702, "lon": 10.1878061 }, + { "lat": 56.1866916, "lon": 10.1877889 }, + { "lat": 56.1866631, "lon": 10.1876743 }, + { "lat": 56.1866417, "lon": 10.1876915 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1535146953, + "bounds": { + "minlat": 56.1865696, + "minlon": 10.1877850, + "maxlat": 56.1866106, + "maxlon": 10.1878791 + }, + "geometry": [ + { "lat": 56.1865696, "lon": 10.1878028 }, + { "lat": 56.1865890, "lon": 10.1878791 }, + { "lat": 56.1866106, "lon": 10.1878613 }, + { "lat": 56.1865912, "lon": 10.1877850 }, + { "lat": 56.1865696, "lon": 10.1878028 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1535173941, + "bounds": { + "minlat": 56.0373842, + "minlon": 10.0835391, + "maxlat": 56.0374395, + "maxlon": 10.0836188 + }, + "geometry": [ + { "lat": 56.0374395, "lon": 10.0835662 }, + { "lat": 56.0373939, "lon": 10.0835391 }, + { "lat": 56.0373842, "lon": 10.0835918 }, + { "lat": 56.0374296, "lon": 10.0836188 }, + { "lat": 56.0374395, "lon": 10.0835662 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1535173942, + "bounds": { + "minlat": 56.0373753, + "minlon": 10.0835918, + "maxlat": 56.0374296, + "maxlon": 10.0836673 + }, + "geometry": [ + { "lat": 56.0374296, "lon": 10.0836188 }, + { "lat": 56.0373842, "lon": 10.0835918 }, + { "lat": 56.0373753, "lon": 10.0836397 }, + { "lat": 56.0374204, "lon": 10.0836673 }, + { "lat": 56.0374296, "lon": 10.0836188 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1542835256, + "bounds": { + "minlat": 56.1626511, + "minlon": 10.1338086, + "maxlat": 56.1627021, + "maxlon": 10.1338672 + }, + "geometry": [ + { "lat": 56.1626547, "lon": 10.1338672 }, + { "lat": 56.1627021, "lon": 10.1338553 }, + { "lat": 56.1626984, "lon": 10.1338086 }, + { "lat": 56.1626511, "lon": 10.1338205 }, + { "lat": 56.1626547, "lon": 10.1338672 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1542835257, + "bounds": { + "minlat": 56.1626476, + "minlon": 10.1337627, + "maxlat": 56.1626984, + "maxlon": 10.1338205 + }, + "geometry": [ + { "lat": 56.1626984, "lon": 10.1338086 }, + { "lat": 56.1626947, "lon": 10.1337627 }, + { "lat": 56.1626476, "lon": 10.1337740 }, + { "lat": 56.1626511, "lon": 10.1338205 }, + { "lat": 56.1626984, "lon": 10.1338086 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1545847758, + "bounds": { + "minlat": 56.2759307, + "minlon": 10.3149898, + "maxlat": 56.2759985, + "maxlon": 10.3151366 + }, + "geometry": [ + { "lat": 56.2759985, "lon": 10.3150220 }, + { "lat": 56.2759772, "lon": 10.3151021 }, + { "lat": 56.2759680, "lon": 10.3151366 }, + { "lat": 56.2759307, "lon": 10.3151044 }, + { "lat": 56.2759413, "lon": 10.3150645 }, + { "lat": 56.2759612, "lon": 10.3149898 }, + { "lat": 56.2759985, "lon": 10.3150220 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1545847759, + "bounds": { + "minlat": 56.2758944, + "minlon": 10.3149496, + "maxlat": 56.2759418, + "maxlon": 10.3150462 + }, + "geometry": [ + { "lat": 56.2759418, "lon": 10.3149745 }, + { "lat": 56.2759215, "lon": 10.3150462 }, + { "lat": 56.2758944, "lon": 10.3150214 }, + { "lat": 56.2759146, "lon": 10.3149496 }, + { "lat": 56.2759418, "lon": 10.3149745 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1545848761, + "bounds": { + "minlat": 56.2760761, + "minlon": 10.3159819, + "maxlat": 56.2761254, + "maxlon": 10.3160779 + }, + "geometry": [ + { "lat": 56.2761254, "lon": 10.3160105 }, + { "lat": 56.2761045, "lon": 10.3160779 }, + { "lat": 56.2760761, "lon": 10.3160493 }, + { "lat": 56.2760970, "lon": 10.3159819 }, + { "lat": 56.2761254, "lon": 10.3160105 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1545848762, + "bounds": { + "minlat": 56.2760153, + "minlon": 10.3159208, + "maxlat": 56.2760970, + "maxlon": 10.3160493 + }, + "geometry": [ + { "lat": 56.2760970, "lon": 10.3159819 }, + { "lat": 56.2760761, "lon": 10.3160493 }, + { "lat": 56.2760153, "lon": 10.3159875 }, + { "lat": 56.2760360, "lon": 10.3159208 }, + { "lat": 56.2760970, "lon": 10.3159819 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1546927438, + "bounds": { + "minlat": 56.1194143, + "minlon": 10.1818885, + "maxlat": 56.1194611, + "maxlon": 10.1819822 + }, + "geometry": [ + { "lat": 56.1194143, "lon": 10.1819113 }, + { "lat": 56.1194318, "lon": 10.1819822 }, + { "lat": 56.1194611, "lon": 10.1819590 }, + { "lat": 56.1194431, "lon": 10.1818885 }, + { "lat": 56.1194143, "lon": 10.1819113 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555040008, + "bounds": { + "minlat": 56.1731411, + "minlon": 10.2087502, + "maxlat": 56.1731766, + "maxlon": 10.2088197 + }, + "geometry": [ + { "lat": 56.1731443, "lon": 10.2088180 }, + { "lat": 56.1731675, "lon": 10.2088197 }, + { "lat": 56.1731766, "lon": 10.2087551 }, + { "lat": 56.1731411, "lon": 10.2087502 }, + { "lat": 56.1731443, "lon": 10.2088180 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated", + "orientation": "perpendicular", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1555040009, + "bounds": { + "minlat": 56.1730855, + "minlon": 10.2091814, + "maxlat": 56.1731335, + "maxlon": 10.2092506 + }, + "geometry": [ + { "lat": 56.1730855, "lon": 10.2092133 }, + { "lat": 56.1730952, "lon": 10.2092506 }, + { "lat": 56.1731335, "lon": 10.2092186 }, + { "lat": 56.1731239, "lon": 10.2091814 }, + { "lat": 56.1730855, "lon": 10.2092133 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "disabled": "designated" + } +}, +{ + "type": "way", + "id": 1555041330, + "bounds": { + "minlat": 56.1664534, + "minlon": 10.2132835, + "maxlat": 56.1666157, + "maxlon": 10.2134602 + }, + "geometry": [ + { "lat": 56.1665995, "lon": 10.2134602 }, + { "lat": 56.1666157, "lon": 10.2133858 }, + { "lat": 56.1665877, "lon": 10.2133662 }, + { "lat": 56.1664696, "lon": 10.2132835 }, + { "lat": 56.1664534, "lon": 10.2133579 }, + { "lat": 56.1665722, "lon": 10.2134411 }, + { "lat": 56.1665995, "lon": 10.2134602 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1555041331, + "bounds": { + "minlat": 56.1665722, + "minlon": 10.2133662, + "maxlat": 56.1666157, + "maxlon": 10.2134602 + }, + "geometry": [ + { "lat": 56.1665722, "lon": 10.2134411 }, + { "lat": 56.1665877, "lon": 10.2133662 }, + { "lat": 56.1666157, "lon": 10.2133858 }, + { "lat": 56.1665995, "lon": 10.2134602 }, + { "lat": 56.1665722, "lon": 10.2134411 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555041332, + "bounds": { + "minlat": 56.1666521, + "minlon": 10.2133818, + "maxlat": 56.1667198, + "maxlon": 10.2135485 + }, + "geometry": [ + { "lat": 56.1666521, "lon": 10.2135225 }, + { "lat": 56.1666893, "lon": 10.2135485 }, + { "lat": 56.1667021, "lon": 10.2134892 }, + { "lat": 56.1667198, "lon": 10.2134079 }, + { "lat": 56.1666826, "lon": 10.2133818 }, + { "lat": 56.1666649, "lon": 10.2134635 }, + { "lat": 56.1666521, "lon": 10.2135225 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1555041333, + "bounds": { + "minlat": 56.1666521, + "minlon": 10.2134635, + "maxlat": 56.1667021, + "maxlon": 10.2135485 + }, + "geometry": [ + { "lat": 56.1666521, "lon": 10.2135225 }, + { "lat": 56.1666893, "lon": 10.2135485 }, + { "lat": 56.1667021, "lon": 10.2134892 }, + { "lat": 56.1666649, "lon": 10.2134635 }, + { "lat": 56.1666521, "lon": 10.2135225 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555041334, + "bounds": { + "minlat": 56.1667085, + "minlon": 10.2121686, + "maxlat": 56.1668412, + "maxlon": 10.2123287 + }, + "geometry": [ + { "lat": 56.1667085, "lon": 10.2122409 }, + { "lat": 56.1667904, "lon": 10.2123031 }, + { "lat": 56.1668241, "lon": 10.2123287 }, + { "lat": 56.1668412, "lon": 10.2122564 }, + { "lat": 56.1668061, "lon": 10.2122298 }, + { "lat": 56.1667255, "lon": 10.2121686 }, + { "lat": 56.1667085, "lon": 10.2122409 } + ], + "tags": { + "amenity": "parking", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1555041335, + "bounds": { + "minlat": 56.1667904, + "minlon": 10.2122298, + "maxlat": 56.1668412, + "maxlon": 10.2123287 + }, + "geometry": [ + { "lat": 56.1667904, "lon": 10.2123031 }, + { "lat": 56.1668241, "lon": 10.2123287 }, + { "lat": 56.1668412, "lon": 10.2122564 }, + { "lat": 56.1668061, "lon": 10.2122298 }, + { "lat": 56.1667904, "lon": 10.2123031 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858185, + "bounds": { + "minlat": 56.2623275, + "minlon": 10.3030346, + "maxlat": 56.2624045, + "maxlon": 10.3031851 + }, + "geometry": [ + { "lat": 56.2624045, "lon": 10.3030790 }, + { "lat": 56.2623873, "lon": 10.3031204 }, + { "lat": 56.2623604, "lon": 10.3031851 }, + { "lat": 56.2623275, "lon": 10.3031408 }, + { "lat": 56.2623715, "lon": 10.3030346 }, + { "lat": 56.2624045, "lon": 10.3030790 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858186, + "bounds": { + "minlat": 56.2625358, + "minlon": 10.3011695, + "maxlat": 56.2625821, + "maxlon": 10.3012403 + }, + "geometry": [ + { "lat": 56.2625358, "lon": 10.3012278 }, + { "lat": 56.2625412, "lon": 10.3011695 }, + { "lat": 56.2625821, "lon": 10.3011820 }, + { "lat": 56.2625766, "lon": 10.3012403 }, + { "lat": 56.2625358, "lon": 10.3012278 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858187, + "bounds": { + "minlat": 56.2711213, + "minlon": 10.3082459, + "maxlat": 56.2711846, + "maxlon": 10.3083714 + }, + "geometry": [ + { "lat": 56.2711213, "lon": 10.3083369 }, + { "lat": 56.2711590, "lon": 10.3082459 }, + { "lat": 56.2711846, "lon": 10.3082803 }, + { "lat": 56.2711470, "lon": 10.3083714 }, + { "lat": 56.2711213, "lon": 10.3083369 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858188, + "bounds": { + "minlat": 56.2711687, + "minlon": 10.3081165, + "maxlat": 56.2712408, + "maxlon": 10.3082609 + }, + "geometry": [ + { "lat": 56.2711687, "lon": 10.3082246 }, + { "lat": 56.2712141, "lon": 10.3081165 }, + { "lat": 56.2712408, "lon": 10.3081529 }, + { "lat": 56.2711954, "lon": 10.3082609 }, + { "lat": 56.2711687, "lon": 10.3082246 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858189, + "bounds": { + "minlat": 56.2715761, + "minlon": 10.3056582, + "maxlat": 56.2716947, + "maxlon": 10.3058645 + }, + "geometry": [ + { "lat": 56.2716947, "lon": 10.3057116 }, + { "lat": 56.2716038, "lon": 10.3056582 }, + { "lat": 56.2715761, "lon": 10.3058111 }, + { "lat": 56.2716670, "lon": 10.3058645 }, + { "lat": 56.2716947, "lon": 10.3057116 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "4", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858190, + "bounds": { + "minlat": 56.2764714, + "minlon": 10.3155969, + "maxlat": 56.2765163, + "maxlon": 10.3156880 + }, + "geometry": [ + { "lat": 56.2764952, "lon": 10.3155969 }, + { "lat": 56.2765163, "lon": 10.3156215 }, + { "lat": 56.2764926, "lon": 10.3156880 }, + { "lat": 56.2764714, "lon": 10.3156635 }, + { "lat": 56.2764952, "lon": 10.3155969 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858191, + "bounds": { + "minlat": 56.2777774, + "minlon": 10.3181653, + "maxlat": 56.2778216, + "maxlon": 10.3182564 + }, + "geometry": [ + { "lat": 56.2777964, "lon": 10.3182564 }, + { "lat": 56.2778216, "lon": 10.3181881 }, + { "lat": 56.2778025, "lon": 10.3181653 }, + { "lat": 56.2777774, "lon": 10.3182336 }, + { "lat": 56.2777964, "lon": 10.3182564 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555858192, + "bounds": { + "minlat": 56.2756047, + "minlon": 10.3167878, + "maxlat": 56.2756980, + "maxlon": 10.3169582 + }, + "geometry": [ + { "lat": 56.2756389, "lon": 10.3167878 }, + { "lat": 56.2756980, "lon": 10.3168464 }, + { "lat": 56.2756639, "lon": 10.3169582 }, + { "lat": 56.2756047, "lon": 10.3168997 }, + { "lat": 56.2756389, "lon": 10.3167878 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862305, + "bounds": { + "minlat": 56.2233008, + "minlon": 10.2888642, + "maxlat": 56.2233686, + "maxlon": 10.2890289 + }, + "geometry": [ + { "lat": 56.2233008, "lon": 10.2890039 }, + { "lat": 56.2233380, "lon": 10.2888642 }, + { "lat": 56.2233686, "lon": 10.2888907 }, + { "lat": 56.2233298, "lon": 10.2890289 }, + { "lat": 56.2233008, "lon": 10.2890039 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862306, + "bounds": { + "minlat": 56.2116258, + "minlon": 10.2792066, + "maxlat": 56.2116701, + "maxlon": 10.2793009 + }, + "geometry": [ + { "lat": 56.2116701, "lon": 10.2792283 }, + { "lat": 56.2116489, "lon": 10.2793009 }, + { "lat": 56.2116258, "lon": 10.2792791 }, + { "lat": 56.2116470, "lon": 10.2792066 }, + { "lat": 56.2116701, "lon": 10.2792283 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862307, + "bounds": { + "minlat": 56.2119604, + "minlon": 10.2702624, + "maxlat": 56.2120516, + "maxlon": 10.2703909 + }, + "geometry": [ + { "lat": 56.2120516, "lon": 10.2703002 }, + { "lat": 56.2119741, "lon": 10.2703909 }, + { "lat": 56.2119604, "lon": 10.2703532 }, + { "lat": 56.2120380, "lon": 10.2702624 }, + { "lat": 56.2120516, "lon": 10.2703002 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862308, + "bounds": { + "minlat": 56.2051362, + "minlon": 10.2619544, + "maxlat": 56.2052123, + "maxlon": 10.2620165 + }, + "geometry": [ + { "lat": 56.2051487, "lon": 10.2620149 }, + { "lat": 56.2051362, "lon": 10.2619638 }, + { "lat": 56.2051475, "lon": 10.2619544 }, + { "lat": 56.2051959, "lon": 10.2619555 }, + { "lat": 56.2052123, "lon": 10.2620165 }, + { "lat": 56.2051487, "lon": 10.2620149 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862309, + "bounds": { + "minlat": 56.2056185, + "minlon": 10.2614363, + "maxlat": 56.2056694, + "maxlon": 10.2615209 + }, + "geometry": [ + { "lat": 56.2056335, "lon": 10.2615209 }, + { "lat": 56.2056694, "lon": 10.2614854 }, + { "lat": 56.2056544, "lon": 10.2614363 }, + { "lat": 56.2056185, "lon": 10.2614717 }, + { "lat": 56.2056335, "lon": 10.2615209 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862310, + "bounds": { + "minlat": 56.2015474, + "minlon": 10.2608807, + "maxlat": 56.2015967, + "maxlon": 10.2609399 + }, + "geometry": [ + { "lat": 56.2015967, "lon": 10.2609244 }, + { "lat": 56.2015521, "lon": 10.2609399 }, + { "lat": 56.2015474, "lon": 10.2608962 }, + { "lat": 56.2015920, "lon": 10.2608807 }, + { "lat": 56.2015967, "lon": 10.2609244 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862311, + "bounds": { + "minlat": 56.2013477, + "minlon": 10.2604663, + "maxlat": 56.2013949, + "maxlon": 10.2605242 + }, + "geometry": [ + { "lat": 56.2013477, "lon": 10.2604891 }, + { "lat": 56.2013888, "lon": 10.2604663 }, + { "lat": 56.2013949, "lon": 10.2605013 }, + { "lat": 56.2013537, "lon": 10.2605242 }, + { "lat": 56.2013477, "lon": 10.2604891 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862312, + "bounds": { + "minlat": 56.2014448, + "minlon": 10.2601057, + "maxlat": 56.2015098, + "maxlon": 10.2601874 + }, + "geometry": [ + { "lat": 56.2015098, "lon": 10.2601520 }, + { "lat": 56.2014538, "lon": 10.2601874 }, + { "lat": 56.2014448, "lon": 10.2601412 }, + { "lat": 56.2015007, "lon": 10.2601057 }, + { "lat": 56.2015098, "lon": 10.2601520 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862313, + "bounds": { + "minlat": 56.2015159, + "minlon": 10.2449946, + "maxlat": 56.2015759, + "maxlon": 10.2450894 + }, + "geometry": [ + { "lat": 56.2015615, "lon": 10.2449946 }, + { "lat": 56.2015159, "lon": 10.2450306 }, + { "lat": 56.2015302, "lon": 10.2450894 }, + { "lat": 56.2015759, "lon": 10.2450534 }, + { "lat": 56.2015615, "lon": 10.2449946 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862314, + "bounds": { + "minlat": 56.2023578, + "minlon": 10.2467254, + "maxlat": 56.2024058, + "maxlon": 10.2468140 + }, + "geometry": [ + { "lat": 56.2023578, "lon": 10.2467876 }, + { "lat": 56.2023735, "lon": 10.2467254 }, + { "lat": 56.2024058, "lon": 10.2467518 }, + { "lat": 56.2023901, "lon": 10.2468140 }, + { "lat": 56.2023578, "lon": 10.2467876 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862315, + "bounds": { + "minlat": 56.2025000, + "minlon": 10.2458489, + "maxlat": 56.2025537, + "maxlon": 10.2459346 + }, + "geometry": [ + { "lat": 56.2025537, "lon": 10.2458818 }, + { "lat": 56.2025404, "lon": 10.2459346 }, + { "lat": 56.2025000, "lon": 10.2459017 }, + { "lat": 56.2025133, "lon": 10.2458489 }, + { "lat": 56.2025537, "lon": 10.2458818 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862316, + "bounds": { + "minlat": 56.2019924, + "minlon": 10.2453929, + "maxlat": 56.2020398, + "maxlon": 10.2454885 + }, + "geometry": [ + { "lat": 56.2020081, "lon": 10.2453929 }, + { "lat": 56.2020398, "lon": 10.2454148 }, + { "lat": 56.2020241, "lon": 10.2454885 }, + { "lat": 56.2019924, "lon": 10.2454666 }, + { "lat": 56.2020081, "lon": 10.2453929 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862318, + "bounds": { + "minlat": 56.1939226, + "minlon": 10.2237940, + "maxlat": 56.1939777, + "maxlon": 10.2239261 + }, + "geometry": [ + { "lat": 56.1939777, "lon": 10.2238052 }, + { "lat": 56.1939687, "lon": 10.2239261 }, + { "lat": 56.1939226, "lon": 10.2239149 }, + { "lat": 56.1939317, "lon": 10.2237940 }, + { "lat": 56.1939777, "lon": 10.2238052 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862319, + "bounds": { + "minlat": 56.1885078, + "minlon": 10.2151642, + "maxlat": 56.1885912, + "maxlon": 10.2153098 + }, + "geometry": [ + { "lat": 56.1885912, "lon": 10.2152453 }, + { "lat": 56.1885387, "lon": 10.2153098 }, + { "lat": 56.1885078, "lon": 10.2152287 }, + { "lat": 56.1885604, "lon": 10.2151642 }, + { "lat": 56.1885912, "lon": 10.2152453 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862320, + "bounds": { + "minlat": 56.1089457, + "minlon": 10.1542249, + "maxlat": 56.1090002, + "maxlon": 10.1543193 + }, + "geometry": [ + { "lat": 56.1089658, "lon": 10.1542249 }, + { "lat": 56.1090002, "lon": 10.1542744 }, + { "lat": 56.1089801, "lon": 10.1543193 }, + { "lat": 56.1089457, "lon": 10.1542698 }, + { "lat": 56.1089658, "lon": 10.1542249 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862322, + "bounds": { + "minlat": 56.1090709, + "minlon": 10.1542386, + "maxlat": 56.1091336, + "maxlon": 10.1543483 + }, + "geometry": [ + { "lat": 56.1091336, "lon": 10.1542927 }, + { "lat": 56.1091092, "lon": 10.1543483 }, + { "lat": 56.1090709, "lon": 10.1542942 }, + { "lat": 56.1090954, "lon": 10.1542386 }, + { "lat": 56.1091336, "lon": 10.1542927 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555862323, + "bounds": { + "minlat": 56.1030415, + "minlon": 10.1546604, + "maxlat": 56.1030874, + "maxlon": 10.1547513 + }, + "geometry": [ + { "lat": 56.1030874, "lon": 10.1546809 }, + { "lat": 56.1030733, "lon": 10.1547513 }, + { "lat": 56.1030415, "lon": 10.1547308 }, + { "lat": 56.1030556, "lon": 10.1546604 }, + { "lat": 56.1030874, "lon": 10.1546809 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865465, + "bounds": { + "minlat": 56.2286894, + "minlon": 10.2376047, + "maxlat": 56.2287416, + "maxlon": 10.2377261 + }, + "geometry": [ + { "lat": 56.2287362, "lon": 10.2376047 }, + { "lat": 56.2287416, "lon": 10.2377189 }, + { "lat": 56.2286948, "lon": 10.2377261 }, + { "lat": 56.2286894, "lon": 10.2376118 }, + { "lat": 56.2287362, "lon": 10.2376047 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865466, + "bounds": { + "minlat": 56.2368594, + "minlon": 10.2350925, + "maxlat": 56.2369199, + "maxlon": 10.2352141 + }, + "geometry": [ + { "lat": 56.2368778, "lon": 10.2352141 }, + { "lat": 56.2368594, "lon": 10.2351187 }, + { "lat": 56.2369015, "lon": 10.2350925 }, + { "lat": 56.2369199, "lon": 10.2351878 }, + { "lat": 56.2368778, "lon": 10.2352141 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865468, + "bounds": { + "minlat": 56.2038016, + "minlon": 10.2356253, + "maxlat": 56.2038506, + "maxlon": 10.2356900 + }, + "geometry": [ + { "lat": 56.2038506, "lon": 10.2356783 }, + { "lat": 56.2038059, "lon": 10.2356900 }, + { "lat": 56.2038016, "lon": 10.2356370 }, + { "lat": 56.2038463, "lon": 10.2356253 }, + { "lat": 56.2038506, "lon": 10.2356783 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865469, + "bounds": { + "minlat": 56.2433773, + "minlon": 10.2629412, + "maxlat": 56.2434439, + "maxlon": 10.2630772 + }, + "geometry": [ + { "lat": 56.2434214, "lon": 10.2629412 }, + { "lat": 56.2434439, "lon": 10.2630468 }, + { "lat": 56.2433998, "lon": 10.2630772 }, + { "lat": 56.2433773, "lon": 10.2629716 }, + { "lat": 56.2434214, "lon": 10.2629412 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865470, + "bounds": { + "minlat": 56.2467572, + "minlon": 10.2671217, + "maxlat": 56.2468030, + "maxlon": 10.2672125 + }, + "geometry": [ + { "lat": 56.2467875, "lon": 10.2671217 }, + { "lat": 56.2468030, "lon": 10.2671903 }, + { "lat": 56.2467727, "lon": 10.2672125 }, + { "lat": 56.2467572, "lon": 10.2671439 }, + { "lat": 56.2467875, "lon": 10.2671217 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865471, + "bounds": { + "minlat": 56.2098325, + "minlon": 10.2869946, + "maxlat": 56.2098837, + "maxlon": 10.2870813 + }, + "geometry": [ + { "lat": 56.2098837, "lon": 10.2870348 }, + { "lat": 56.2098492, "lon": 10.2870813 }, + { "lat": 56.2098325, "lon": 10.2870411 }, + { "lat": 56.2098670, "lon": 10.2869946 }, + { "lat": 56.2098837, "lon": 10.2870348 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865472, + "bounds": { + "minlat": 56.2102750, + "minlon": 10.2877462, + "maxlat": 56.2103205, + "maxlon": 10.2878359 + }, + "geometry": [ + { "lat": 56.2102976, "lon": 10.2878359 }, + { "lat": 56.2102750, "lon": 10.2878094 }, + { "lat": 56.2102979, "lon": 10.2877462 }, + { "lat": 56.2103205, "lon": 10.2877726 }, + { "lat": 56.2102976, "lon": 10.2878359 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865473, + "bounds": { + "minlat": 56.1679583, + "minlon": 10.1987910, + "maxlat": 56.1679927, + "maxlon": 10.1988780 + }, + "geometry": [ + { "lat": 56.1679783, "lon": 10.1987910 }, + { "lat": 56.1679927, "lon": 10.1988655 }, + { "lat": 56.1679727, "lon": 10.1988780 }, + { "lat": 56.1679583, "lon": 10.1988035 }, + { "lat": 56.1679783, "lon": 10.1987910 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865474, + "bounds": { + "minlat": 56.1669700, + "minlon": 10.1987074, + "maxlat": 56.1670202, + "maxlon": 10.1987888 + }, + "geometry": [ + { "lat": 56.1670202, "lon": 10.1987638 }, + { "lat": 56.1669812, "lon": 10.1987888 }, + { "lat": 56.1669700, "lon": 10.1987325 }, + { "lat": 56.1670090, "lon": 10.1987074 }, + { "lat": 56.1670202, "lon": 10.1987638 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865475, + "bounds": { + "minlat": 56.1665494, + "minlon": 10.1993350, + "maxlat": 56.1665936, + "maxlon": 10.1993944 + }, + "geometry": [ + { "lat": 56.1665494, "lon": 10.1993591 }, + { "lat": 56.1665865, "lon": 10.1993350 }, + { "lat": 56.1665936, "lon": 10.1993703 }, + { "lat": 56.1665565, "lon": 10.1993944 }, + { "lat": 56.1665494, "lon": 10.1993591 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865476, + "bounds": { + "minlat": 56.1666813, + "minlon": 10.1993250, + "maxlat": 56.1667271, + "maxlon": 10.1993902 + }, + "geometry": [ + { "lat": 56.1666813, "lon": 10.1993477 }, + { "lat": 56.1667192, "lon": 10.1993250 }, + { "lat": 56.1667271, "lon": 10.1993675 }, + { "lat": 56.1666891, "lon": 10.1993902 }, + { "lat": 56.1666813, "lon": 10.1993477 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865477, + "bounds": { + "minlat": 56.1663289, + "minlon": 10.1996724, + "maxlat": 56.1663657, + "maxlon": 10.1997406 + }, + "geometry": [ + { "lat": 56.1663289, "lon": 10.1996899 }, + { "lat": 56.1663553, "lon": 10.1996724 }, + { "lat": 56.1663657, "lon": 10.1997230 }, + { "lat": 56.1663394, "lon": 10.1997406 }, + { "lat": 56.1663289, "lon": 10.1996899 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865478, + "bounds": { + "minlat": 56.1662467, + "minlon": 10.1997279, + "maxlat": 56.1662855, + "maxlon": 10.1998011 + }, + "geometry": [ + { "lat": 56.1662744, "lon": 10.1997279 }, + { "lat": 56.1662855, "lon": 10.1997830 }, + { "lat": 56.1662579, "lon": 10.1998011 }, + { "lat": 56.1662467, "lon": 10.1997459 }, + { "lat": 56.1662744, "lon": 10.1997279 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865479, + "bounds": { + "minlat": 56.1660044, + "minlon": 10.1998021, + "maxlat": 56.1660574, + "maxlon": 10.1998780 + }, + "geometry": [ + { "lat": 56.1660574, "lon": 10.1998490 }, + { "lat": 56.1660142, "lon": 10.1998780 }, + { "lat": 56.1660044, "lon": 10.1998310 }, + { "lat": 56.1660477, "lon": 10.1998021 }, + { "lat": 56.1660574, "lon": 10.1998490 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1555865480, + "bounds": { + "minlat": 56.1665610, + "minlon": 10.2037774, + "maxlat": 56.1666149, + "maxlon": 10.2038621 + }, + "geometry": [ + { "lat": 56.1665610, "lon": 10.2038050 }, + { "lat": 56.1666034, "lon": 10.2037774 }, + { "lat": 56.1666149, "lon": 10.2038344 }, + { "lat": 56.1665725, "lon": 10.2038621 }, + { "lat": 56.1665610, "lon": 10.2038050 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556631742, + "bounds": { + "minlat": 56.2659376, + "minlon": 10.3100478, + "maxlat": 56.2659864, + "maxlon": 10.3101153 + }, + "geometry": [ + { "lat": 56.2659420, "lon": 10.3100478 }, + { "lat": 56.2659864, "lon": 10.3100591 }, + { "lat": 56.2659820, "lon": 10.3101153 }, + { "lat": 56.2659376, "lon": 10.3101040 }, + { "lat": 56.2659420, "lon": 10.3100478 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556851595, + "bounds": { + "minlat": 56.1480583, + "minlon": 10.1907230, + "maxlat": 56.1481034, + "maxlon": 10.1908309 + }, + "geometry": [ + { "lat": 56.1480640, "lon": 10.1908309 }, + { "lat": 56.1481034, "lon": 10.1907781 }, + { "lat": 56.1480977, "lon": 10.1907230 }, + { "lat": 56.1480583, "lon": 10.1907775 }, + { "lat": 56.1480640, "lon": 10.1908309 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556851786, + "bounds": { + "minlat": 56.1533285, + "minlon": 10.1795554, + "maxlat": 56.1533797, + "maxlon": 10.1796417 + }, + "geometry": [ + { "lat": 56.1533449, "lon": 10.1796417 }, + { "lat": 56.1533797, "lon": 10.1796032 }, + { "lat": 56.1533634, "lon": 10.1795554 }, + { "lat": 56.1533285, "lon": 10.1795939 }, + { "lat": 56.1533449, "lon": 10.1796417 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853035, + "bounds": { + "minlat": 56.1639920, + "minlon": 10.1748528, + "maxlat": 56.1640262, + "maxlon": 10.1749341 + }, + "geometry": [ + { "lat": 56.1640262, "lon": 10.1749288 }, + { "lat": 56.1640220, "lon": 10.1748528 }, + { "lat": 56.1639920, "lon": 10.1748581 }, + { "lat": 56.1639965, "lon": 10.1749341 }, + { "lat": 56.1640262, "lon": 10.1749288 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853580, + "bounds": { + "minlat": 56.1702474, + "minlon": 10.1753204, + "maxlat": 56.1702824, + "maxlon": 10.1754131 + }, + "geometry": [ + { "lat": 56.1702824, "lon": 10.1754074 }, + { "lat": 56.1702773, "lon": 10.1753204 }, + { "lat": 56.1702474, "lon": 10.1753261 }, + { "lat": 56.1702526, "lon": 10.1754131 }, + { "lat": 56.1702824, "lon": 10.1754074 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853581, + "bounds": { + "minlat": 56.1711904, + "minlon": 10.1750482, + "maxlat": 56.1712229, + "maxlon": 10.1751194 + }, + "geometry": [ + { "lat": 56.1712192, "lon": 10.1750482 }, + { "lat": 56.1712229, "lon": 10.1751142 }, + { "lat": 56.1711941, "lon": 10.1751194 }, + { "lat": 56.1711904, "lon": 10.1750534 }, + { "lat": 56.1712192, "lon": 10.1750482 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853821, + "bounds": { + "minlat": 56.1705614, + "minlon": 10.1762406, + "maxlat": 56.1705874, + "maxlon": 10.1763227 + }, + "geometry": [ + { "lat": 56.1705853, "lon": 10.1762406 }, + { "lat": 56.1705614, "lon": 10.1762428 }, + { "lat": 56.1705634, "lon": 10.1763227 }, + { "lat": 56.1705874, "lon": 10.1763210 }, + { "lat": 56.1705853, "lon": 10.1762406 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853822, + "bounds": { + "minlat": 56.1705390, + "minlon": 10.1762428, + "maxlat": 56.1705634, + "maxlon": 10.1763244 + }, + "geometry": [ + { "lat": 56.1705614, "lon": 10.1762428 }, + { "lat": 56.1705390, "lon": 10.1762445 }, + { "lat": 56.1705409, "lon": 10.1763244 }, + { "lat": 56.1705634, "lon": 10.1763227 }, + { "lat": 56.1705614, "lon": 10.1762428 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853823, + "bounds": { + "minlat": 56.1705171, + "minlon": 10.1762445, + "maxlat": 56.1705409, + "maxlon": 10.1763261 + }, + "geometry": [ + { "lat": 56.1705390, "lon": 10.1762445 }, + { "lat": 56.1705171, "lon": 10.1762462 }, + { "lat": 56.1705190, "lon": 10.1763261 }, + { "lat": 56.1705409, "lon": 10.1763244 }, + { "lat": 56.1705390, "lon": 10.1762445 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853824, + "bounds": { + "minlat": 56.1704961, + "minlon": 10.1762462, + "maxlat": 56.1705190, + "maxlon": 10.1763277 + }, + "geometry": [ + { "lat": 56.1705171, "lon": 10.1762462 }, + { "lat": 56.1704961, "lon": 10.1762478 }, + { "lat": 56.1704980, "lon": 10.1763277 }, + { "lat": 56.1705190, "lon": 10.1763261 }, + { "lat": 56.1705171, "lon": 10.1762462 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853825, + "bounds": { + "minlat": 56.1703966, + "minlon": 10.1762570, + "maxlat": 56.1704307, + "maxlon": 10.1763406 + }, + "geometry": [ + { "lat": 56.1704280, "lon": 10.1762570 }, + { "lat": 56.1704307, "lon": 10.1763372 }, + { "lat": 56.1703993, "lon": 10.1763406 }, + { "lat": 56.1703966, "lon": 10.1762605 }, + { "lat": 56.1704280, "lon": 10.1762570 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556853826, + "bounds": { + "minlat": 56.1703643, + "minlon": 10.1762605, + "maxlat": 56.1703993, + "maxlon": 10.1763442 + }, + "geometry": [ + { "lat": 56.1703966, "lon": 10.1762605 }, + { "lat": 56.1703993, "lon": 10.1763406 }, + { "lat": 56.1703671, "lon": 10.1763442 }, + { "lat": 56.1703643, "lon": 10.1762640 }, + { "lat": 56.1703966, "lon": 10.1762605 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1556854684, + "bounds": { + "minlat": 56.1507693, + "minlon": 10.2075242, + "maxlat": 56.1508207, + "maxlon": 10.2075954 + }, + "geometry": [ + { "lat": 56.1507693, "lon": 10.2075418 }, + { "lat": 56.1507758, "lon": 10.2075954 }, + { "lat": 56.1508207, "lon": 10.2075778 }, + { "lat": 56.1508142, "lon": 10.2075242 }, + { "lat": 56.1507693, "lon": 10.2075418 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557150137, + "bounds": { + "minlat": 56.1938470, + "minlon": 10.1846739, + "maxlat": 56.1938967, + "maxlon": 10.1847448 + }, + "geometry": [ + { "lat": 56.1938567, "lon": 10.1847448 }, + { "lat": 56.1938967, "lon": 10.1847141 }, + { "lat": 56.1938875, "lon": 10.1846739 }, + { "lat": 56.1938470, "lon": 10.1847042 }, + { "lat": 56.1938567, "lon": 10.1847448 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557150138, + "bounds": { + "minlat": 56.1931329, + "minlon": 10.1852065, + "maxlat": 56.1931868, + "maxlon": 10.1852880 + }, + "geometry": [ + { "lat": 56.1931868, "lon": 10.1852547 }, + { "lat": 56.1931447, "lon": 10.1852880 }, + { "lat": 56.1931329, "lon": 10.1852399 }, + { "lat": 56.1931749, "lon": 10.1852065 }, + { "lat": 56.1931868, "lon": 10.1852547 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557175521, + "bounds": { + "minlat": 56.1946699, + "minlon": 10.1839652, + "maxlat": 56.1947270, + "maxlon": 10.1840495 + }, + "geometry": [ + { "lat": 56.1947157, "lon": 10.1839652 }, + { "lat": 56.1946699, "lon": 10.1839975 }, + { "lat": 56.1946822, "lon": 10.1840495 }, + { "lat": 56.1947270, "lon": 10.1840161 }, + { "lat": 56.1947157, "lon": 10.1839652 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557175522, + "bounds": { + "minlat": 56.1945763, + "minlon": 10.1840378, + "maxlat": 56.1946282, + "maxlon": 10.1841189 + }, + "geometry": [ + { "lat": 56.1946164, "lon": 10.1840378 }, + { "lat": 56.1946282, "lon": 10.1840893 }, + { "lat": 56.1945881, "lon": 10.1841189 }, + { "lat": 56.1945763, "lon": 10.1840675 }, + { "lat": 56.1946164, "lon": 10.1840378 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557175523, + "bounds": { + "minlat": 56.1945211, + "minlon": 10.1840835, + "maxlat": 56.1945695, + "maxlon": 10.1841599 + }, + "geometry": [ + { "lat": 56.1945576, "lon": 10.1840835 }, + { "lat": 56.1945211, "lon": 10.1841103 }, + { "lat": 56.1945332, "lon": 10.1841599 }, + { "lat": 56.1945695, "lon": 10.1841328 }, + { "lat": 56.1945576, "lon": 10.1840835 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557175524, + "bounds": { + "minlat": 56.1944248, + "minlon": 10.1841566, + "maxlat": 56.1944705, + "maxlon": 10.1842324 + }, + "geometry": [ + { "lat": 56.1944594, "lon": 10.1841566 }, + { "lat": 56.1944248, "lon": 10.1841832 }, + { "lat": 56.1944361, "lon": 10.1842324 }, + { "lat": 56.1944705, "lon": 10.1842067 }, + { "lat": 56.1944594, "lon": 10.1841566 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557175525, + "bounds": { + "minlat": 56.1943670, + "minlon": 10.1841961, + "maxlat": 56.1944182, + "maxlon": 10.1842757 + }, + "geometry": [ + { "lat": 56.1944069, "lon": 10.1841961 }, + { "lat": 56.1943670, "lon": 10.1842282 }, + { "lat": 56.1943781, "lon": 10.1842757 }, + { "lat": 56.1944182, "lon": 10.1842458 }, + { "lat": 56.1944069, "lon": 10.1841961 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557192764, + "bounds": { + "minlat": 56.1876012, + "minlon": 10.1856183, + "maxlat": 56.1876405, + "maxlon": 10.1856930 + }, + "geometry": [ + { "lat": 56.1876012, "lon": 10.1856413 }, + { "lat": 56.1876213, "lon": 10.1856930 }, + { "lat": 56.1876405, "lon": 10.1856666 }, + { "lat": 56.1876207, "lon": 10.1856183 }, + { "lat": 56.1876012, "lon": 10.1856413 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557192765, + "bounds": { + "minlat": 56.1875074, + "minlon": 10.1857288, + "maxlat": 56.1875450, + "maxlon": 10.1858021 + }, + "geometry": [ + { "lat": 56.1875450, "lon": 10.1857778 }, + { "lat": 56.1875271, "lon": 10.1857288 }, + { "lat": 56.1875074, "lon": 10.1857521 }, + { "lat": 56.1875247, "lon": 10.1858021 }, + { "lat": 56.1875450, "lon": 10.1857778 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557193694, + "bounds": { + "minlat": 56.1876494, + "minlon": 10.1873705, + "maxlat": 56.1876961, + "maxlon": 10.1874529 + }, + "geometry": [ + { "lat": 56.1876494, "lon": 10.1873968 }, + { "lat": 56.1876836, "lon": 10.1873705 }, + { "lat": 56.1876961, "lon": 10.1874256 }, + { "lat": 56.1876626, "lon": 10.1874529 }, + { "lat": 56.1876494, "lon": 10.1873968 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557193695, + "bounds": { + "minlat": 56.1876402, + "minlon": 10.1873319, + "maxlat": 56.1876836, + "maxlon": 10.1873968 + }, + "geometry": [ + { "lat": 56.1876836, "lon": 10.1873705 }, + { "lat": 56.1876747, "lon": 10.1873319 }, + { "lat": 56.1876402, "lon": 10.1873575 }, + { "lat": 56.1876494, "lon": 10.1873968 }, + { "lat": 56.1876836, "lon": 10.1873705 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557193699, + "bounds": { + "minlat": 56.1872725, + "minlon": 10.1868785, + "maxlat": 56.1873301, + "maxlon": 10.1869859 + }, + "geometry": [ + { "lat": 56.1873123, "lon": 10.1868785 }, + { "lat": 56.1872725, "lon": 10.1869084 }, + { "lat": 56.1872906, "lon": 10.1869859 }, + { "lat": 56.1873301, "lon": 10.1869546 }, + { "lat": 56.1873123, "lon": 10.1868785 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557194273, + "bounds": { + "minlat": 56.1869941, + "minlon": 10.1863079, + "maxlat": 56.1870409, + "maxlon": 10.1863809 + }, + "geometry": [ + { "lat": 56.1870045, "lon": 10.1863809 }, + { "lat": 56.1870409, "lon": 10.1863554 }, + { "lat": 56.1870297, "lon": 10.1863079 }, + { "lat": 56.1869941, "lon": 10.1863366 }, + { "lat": 56.1870045, "lon": 10.1863809 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557194274, + "bounds": { + "minlat": 56.1868854, + "minlon": 10.1858434, + "maxlat": 56.1869436, + "maxlon": 10.1859416 + }, + "geometry": [ + { "lat": 56.1869436, "lon": 10.1859099 }, + { "lat": 56.1869005, "lon": 10.1859416 }, + { "lat": 56.1868854, "lon": 10.1858750 }, + { "lat": 56.1869285, "lon": 10.1858434 }, + { "lat": 56.1869436, "lon": 10.1859099 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557195312, + "bounds": { + "minlat": 56.1894669, + "minlon": 10.1866598, + "maxlat": 56.1895158, + "maxlon": 10.1867541 + }, + "geometry": [ + { "lat": 56.1894881, "lon": 10.1867541 }, + { "lat": 56.1895158, "lon": 10.1867251 }, + { "lat": 56.1894946, "lon": 10.1866598 }, + { "lat": 56.1894669, "lon": 10.1866889 }, + { "lat": 56.1894881, "lon": 10.1867541 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557195313, + "bounds": { + "minlat": 56.1894946, + "minlon": 10.1866337, + "maxlat": 56.1895411, + "maxlon": 10.1867251 + }, + "geometry": [ + { "lat": 56.1895158, "lon": 10.1867251 }, + { "lat": 56.1895411, "lon": 10.1866977 }, + { "lat": 56.1895201, "lon": 10.1866337 }, + { "lat": 56.1894946, "lon": 10.1866598 }, + { "lat": 56.1895158, "lon": 10.1867251 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557197538, + "bounds": { + "minlat": 56.1818072, + "minlon": 10.1909888, + "maxlat": 56.1818636, + "maxlon": 10.1910804 + }, + "geometry": [ + { "lat": 56.1818233, "lon": 10.1910804 }, + { "lat": 56.1818636, "lon": 10.1910349 }, + { "lat": 56.1818475, "lon": 10.1909888 }, + { "lat": 56.1818072, "lon": 10.1910343 }, + { "lat": 56.1818233, "lon": 10.1910804 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557197539, + "bounds": { + "minlat": 56.1817906, + "minlon": 10.1909409, + "maxlat": 56.1818475, + "maxlon": 10.1910343 + }, + "geometry": [ + { "lat": 56.1818475, "lon": 10.1909888 }, + { "lat": 56.1818306, "lon": 10.1909409 }, + { "lat": 56.1817906, "lon": 10.1909866 }, + { "lat": 56.1818072, "lon": 10.1910343 }, + { "lat": 56.1818475, "lon": 10.1909888 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557197540, + "bounds": { + "minlat": 56.1817748, + "minlon": 10.1908964, + "maxlat": 56.1818306, + "maxlon": 10.1909866 + }, + "geometry": [ + { "lat": 56.1818306, "lon": 10.1909409 }, + { "lat": 56.1818148, "lon": 10.1908964 }, + { "lat": 56.1817748, "lon": 10.1909415 }, + { "lat": 56.1817906, "lon": 10.1909866 }, + { "lat": 56.1818306, "lon": 10.1909409 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557197541, + "bounds": { + "minlat": 56.1817584, + "minlon": 10.1908499, + "maxlat": 56.1818148, + "maxlon": 10.1909415 + }, + "geometry": [ + { "lat": 56.1818148, "lon": 10.1908964 }, + { "lat": 56.1817988, "lon": 10.1908499 }, + { "lat": 56.1817584, "lon": 10.1908954 }, + { "lat": 56.1817748, "lon": 10.1909415 }, + { "lat": 56.1818148, "lon": 10.1908964 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557197542, + "bounds": { + "minlat": 56.1818084, + "minlon": 10.1905679, + "maxlat": 56.1818637, + "maxlon": 10.1906595 + }, + "geometry": [ + { "lat": 56.1818084, "lon": 10.1906101 }, + { "lat": 56.1818470, "lon": 10.1905679 }, + { "lat": 56.1818637, "lon": 10.1906173 }, + { "lat": 56.1818251, "lon": 10.1906595 }, + { "lat": 56.1818084, "lon": 10.1906101 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557197543, + "bounds": { + "minlat": 56.1818251, + "minlon": 10.1906173, + "maxlat": 56.1818796, + "maxlon": 10.1907060 + }, + "geometry": [ + { "lat": 56.1818251, "lon": 10.1906595 }, + { "lat": 56.1818408, "lon": 10.1907060 }, + { "lat": 56.1818796, "lon": 10.1906643 }, + { "lat": 56.1818637, "lon": 10.1906173 }, + { "lat": 56.1818251, "lon": 10.1906595 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557409609, + "bounds": { + "minlat": 56.1563177, + "minlon": 10.1647235, + "maxlat": 56.1563700, + "maxlon": 10.1648115 + }, + "geometry": [ + { "lat": 56.1563177, "lon": 10.1647342 }, + { "lat": 56.1563645, "lon": 10.1647235 }, + { "lat": 56.1563700, "lon": 10.1648008 }, + { "lat": 56.1563232, "lon": 10.1648115 }, + { "lat": 56.1563177, "lon": 10.1647342 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557410057, + "bounds": { + "minlat": 56.1724554, + "minlon": 10.1549114, + "maxlat": 56.1724991, + "maxlon": 10.1550043 + }, + "geometry": [ + { "lat": 56.1724554, "lon": 10.1549818 }, + { "lat": 56.1724770, "lon": 10.1550043 }, + { "lat": 56.1724991, "lon": 10.1549337 }, + { "lat": 56.1724778, "lon": 10.1549114 }, + { "lat": 56.1724554, "lon": 10.1549818 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557410058, + "bounds": { + "minlat": 56.1724659, + "minlon": 10.1543696, + "maxlat": 56.1725039, + "maxlon": 10.1544546 + }, + "geometry": [ + { "lat": 56.1724659, "lon": 10.1544360 }, + { "lat": 56.1724837, "lon": 10.1544546 }, + { "lat": 56.1725039, "lon": 10.1543877 }, + { "lat": 56.1724866, "lon": 10.1543696 }, + { "lat": 56.1724659, "lon": 10.1544360 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557411322, + "bounds": { + "minlat": 56.1651876, + "minlon": 10.1828125, + "maxlat": 56.1652419, + "maxlon": 10.1829149 + }, + "geometry": [ + { "lat": 56.1652063, "lon": 10.1829028 }, + { "lat": 56.1652419, "lon": 10.1829149 }, + { "lat": 56.1652227, "lon": 10.1828225 }, + { "lat": 56.1651876, "lon": 10.1828125 }, + { "lat": 56.1652063, "lon": 10.1829028 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557411323, + "bounds": { + "minlat": 56.1648269, + "minlon": 10.1830548, + "maxlat": 56.1648865, + "maxlon": 10.1831510 + }, + "geometry": [ + { "lat": 56.1648532, "lon": 10.1831421 }, + { "lat": 56.1648269, "lon": 10.1830548 }, + { "lat": 56.1648660, "lon": 10.1830675 }, + { "lat": 56.1648865, "lon": 10.1831510 }, + { "lat": 56.1648532, "lon": 10.1831421 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557412890, + "bounds": { + "minlat": 56.1628907, + "minlon": 10.1822928, + "maxlat": 56.1629384, + "maxlon": 10.1823651 + }, + "geometry": [ + { "lat": 56.1629322, "lon": 10.1823651 }, + { "lat": 56.1629384, "lon": 10.1823073 }, + { "lat": 56.1628969, "lon": 10.1822928 }, + { "lat": 56.1628907, "lon": 10.1823506 }, + { "lat": 56.1629322, "lon": 10.1823651 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557412891, + "bounds": { + "minlat": 56.1628839, + "minlon": 10.1823506, + "maxlat": 56.1629322, + "maxlon": 10.1824278 + }, + "geometry": [ + { "lat": 56.1629322, "lon": 10.1823651 }, + { "lat": 56.1629255, "lon": 10.1824278 }, + { "lat": 56.1628839, "lon": 10.1824136 }, + { "lat": 56.1628907, "lon": 10.1823506 }, + { "lat": 56.1629322, "lon": 10.1823651 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557412892, + "bounds": { + "minlat": 56.1628775, + "minlon": 10.1824136, + "maxlat": 56.1629255, + "maxlon": 10.1824889 + }, + "geometry": [ + { "lat": 56.1629255, "lon": 10.1824278 }, + { "lat": 56.1629189, "lon": 10.1824889 }, + { "lat": 56.1628775, "lon": 10.1824751 }, + { "lat": 56.1628839, "lon": 10.1824136 }, + { "lat": 56.1629255, "lon": 10.1824278 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557413726, + "bounds": { + "minlat": 56.1724474, + "minlon": 10.2031746, + "maxlat": 56.1724917, + "maxlon": 10.2032965 + }, + "geometry": [ + { "lat": 56.1724917, "lon": 10.2032818 }, + { "lat": 56.1724702, "lon": 10.2032965 }, + { "lat": 56.1724474, "lon": 10.2031894 }, + { "lat": 56.1724689, "lon": 10.2031746 }, + { "lat": 56.1724917, "lon": 10.2032818 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557413727, + "bounds": { + "minlat": 56.1720780, + "minlon": 10.2033480, + "maxlat": 56.1721275, + "maxlon": 10.2034308 + }, + "geometry": [ + { "lat": 56.1721275, "lon": 10.2034046 }, + { "lat": 56.1720888, "lon": 10.2034308 }, + { "lat": 56.1720780, "lon": 10.2033750 }, + { "lat": 56.1721158, "lon": 10.2033480 }, + { "lat": 56.1721275, "lon": 10.2034046 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557431864, + "bounds": { + "minlat": 56.1988629, + "minlon": 10.1805607, + "maxlat": 56.1989150, + "maxlon": 10.1806656 + }, + "geometry": [ + { "lat": 56.1988930, "lon": 10.1805607 }, + { "lat": 56.1989150, "lon": 10.1806378 }, + { "lat": 56.1988853, "lon": 10.1806656 }, + { "lat": 56.1988676, "lon": 10.1806067 }, + { "lat": 56.1988629, "lon": 10.1805901 }, + { "lat": 56.1988930, "lon": 10.1805607 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557431865, + "bounds": { + "minlat": 56.1988853, + "minlon": 10.1806378, + "maxlat": 56.1989368, + "maxlon": 10.1807407 + }, + "geometry": [ + { "lat": 56.1989150, "lon": 10.1806378 }, + { "lat": 56.1989368, "lon": 10.1807129 }, + { "lat": 56.1989072, "lon": 10.1807407 }, + { "lat": 56.1988853, "lon": 10.1806656 }, + { "lat": 56.1989150, "lon": 10.1806378 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557431866, + "bounds": { + "minlat": 56.1989419, + "minlon": 10.1808240, + "maxlat": 56.1989909, + "maxlon": 10.1809237 + }, + "geometry": [ + { "lat": 56.1989702, "lon": 10.1808240 }, + { "lat": 56.1989419, "lon": 10.1808497 }, + { "lat": 56.1989631, "lon": 10.1809237 }, + { "lat": 56.1989909, "lon": 10.1808978 }, + { "lat": 56.1989702, "lon": 10.1808240 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557431867, + "bounds": { + "minlat": 56.1989631, + "minlon": 10.1808978, + "maxlat": 56.1990115, + "maxlon": 10.1809950 + }, + "geometry": [ + { "lat": 56.1989909, "lon": 10.1808978 }, + { "lat": 56.1990115, "lon": 10.1809691 }, + { "lat": 56.1989837, "lon": 10.1809950 }, + { "lat": 56.1989631, "lon": 10.1809237 }, + { "lat": 56.1989909, "lon": 10.1808978 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557431868, + "bounds": { + "minlat": 56.1990187, + "minlon": 10.1810766, + "maxlat": 56.1990637, + "maxlon": 10.1811670 + }, + "geometry": [ + { "lat": 56.1990443, "lon": 10.1810766 }, + { "lat": 56.1990187, "lon": 10.1811048 }, + { "lat": 56.1990378, "lon": 10.1811670 }, + { "lat": 56.1990637, "lon": 10.1811416 }, + { "lat": 56.1990443, "lon": 10.1810766 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557433256, + "bounds": { + "minlat": 56.1926140, + "minlon": 10.1741668, + "maxlat": 56.1926650, + "maxlon": 10.1742773 + }, + "geometry": [ + { "lat": 56.1926140, "lon": 10.1741887 }, + { "lat": 56.1926324, "lon": 10.1742773 }, + { "lat": 56.1926650, "lon": 10.1742554 }, + { "lat": 56.1926466, "lon": 10.1741668 }, + { "lat": 56.1926140, "lon": 10.1741887 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557434490, + "bounds": { + "minlat": 56.1962256, + "minlon": 10.1763699, + "maxlat": 56.1962742, + "maxlon": 10.1764977 + }, + "geometry": [ + { "lat": 56.1962454, "lon": 10.1764977 }, + { "lat": 56.1962402, "lon": 10.1764685 }, + { "lat": 56.1962256, "lon": 10.1763865 }, + { "lat": 56.1962544, "lon": 10.1763699 }, + { "lat": 56.1962742, "lon": 10.1764812 }, + { "lat": 56.1962454, "lon": 10.1764977 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557434491, + "bounds": { + "minlat": 56.1962544, + "minlon": 10.1763537, + "maxlat": 56.1963027, + "maxlon": 10.1764812 + }, + "geometry": [ + { "lat": 56.1962742, "lon": 10.1764812 }, + { "lat": 56.1963027, "lon": 10.1764645 }, + { "lat": 56.1962971, "lon": 10.1764327 }, + { "lat": 56.1962832, "lon": 10.1763537 }, + { "lat": 56.1962544, "lon": 10.1763699 }, + { "lat": 56.1962742, "lon": 10.1764812 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557434492, + "bounds": { + "minlat": 56.1963455, + "minlon": 10.1762909, + "maxlat": 56.1963917, + "maxlon": 10.1763927 + }, + "geometry": [ + { "lat": 56.1963611, "lon": 10.1763927 }, + { "lat": 56.1963455, "lon": 10.1763094 }, + { "lat": 56.1963761, "lon": 10.1762909 }, + { "lat": 56.1963917, "lon": 10.1763742 }, + { "lat": 56.1963611, "lon": 10.1763927 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557434644, + "bounds": { + "minlat": 56.1944073, + "minlon": 10.1723015, + "maxlat": 56.1944844, + "maxlon": 10.1723849 + }, + "geometry": [ + { "lat": 56.1944832, "lon": 10.1723849 }, + { "lat": 56.1944073, "lon": 10.1723744 }, + { "lat": 56.1944099, "lon": 10.1723015 }, + { "lat": 56.1944363, "lon": 10.1723027 }, + { "lat": 56.1944844, "lon": 10.1723050 }, + { "lat": 56.1944832, "lon": 10.1723849 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557434645, + "bounds": { + "minlat": 56.1944363, + "minlon": 10.1722419, + "maxlat": 56.1944854, + "maxlon": 10.1723050 + }, + "geometry": [ + { "lat": 56.1944363, "lon": 10.1723027 }, + { "lat": 56.1944370, "lon": 10.1722419 }, + { "lat": 56.1944854, "lon": 10.1722419 }, + { "lat": 56.1944844, "lon": 10.1723050 }, + { "lat": 56.1944363, "lon": 10.1723027 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557434646, + "bounds": { + "minlat": 56.1944370, + "minlon": 10.1721788, + "maxlat": 56.1944863, + "maxlon": 10.1722419 + }, + "geometry": [ + { "lat": 56.1944370, "lon": 10.1722419 }, + { "lat": 56.1944373, "lon": 10.1721840 }, + { "lat": 56.1944863, "lon": 10.1721788 }, + { "lat": 56.1944854, "lon": 10.1722419 }, + { "lat": 56.1944370, "lon": 10.1722419 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557468737, + "bounds": { + "minlat": 56.1761614, + "minlon": 10.2052042, + "maxlat": 56.1761896, + "maxlon": 10.2052833 + }, + "geometry": [ + { "lat": 56.1761896, "lon": 10.2052125 }, + { "lat": 56.1761715, "lon": 10.2052042 }, + { "lat": 56.1761614, "lon": 10.2052750 }, + { "lat": 56.1761796, "lon": 10.2052833 }, + { "lat": 56.1761896, "lon": 10.2052125 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "orientation": "parallel", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1557469915, + "bounds": { + "minlat": 56.1672789, + "minlon": 10.1848130, + "maxlat": 56.1673340, + "maxlon": 10.1849102 + }, + "geometry": [ + { "lat": 56.1672922, "lon": 10.1849102 }, + { "lat": 56.1672789, "lon": 10.1848855 }, + { "lat": 56.1673208, "lon": 10.1848130 }, + { "lat": 56.1673340, "lon": 10.1848376 }, + { "lat": 56.1672922, "lon": 10.1849102 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1557470158, + "bounds": { + "minlat": 56.1699250, + "minlon": 10.1848106, + "maxlat": 56.1699737, + "maxlon": 10.1849044 + }, + "geometry": [ + { "lat": 56.1699250, "lon": 10.1848932 }, + { "lat": 56.1699341, "lon": 10.1848106 }, + { "lat": 56.1699737, "lon": 10.1848212 }, + { "lat": 56.1699652, "lon": 10.1849044 }, + { "lat": 56.1699250, "lon": 10.1848932 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557470159, + "bounds": { + "minlat": 56.1699652, + "minlon": 10.1848212, + "maxlat": 56.1700069, + "maxlon": 10.1849137 + }, + "geometry": [ + { "lat": 56.1699652, "lon": 10.1849044 }, + { "lat": 56.1699989, "lon": 10.1849137 }, + { "lat": 56.1700069, "lon": 10.1848318 }, + { "lat": 56.1699737, "lon": 10.1848212 }, + { "lat": 56.1699652, "lon": 10.1849044 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557531330, + "bounds": { + "minlat": 56.1535344, + "minlon": 10.1354133, + "maxlat": 56.1535760, + "maxlon": 10.1355015 + }, + "geometry": [ + { "lat": 56.1535693, "lon": 10.1354133 }, + { "lat": 56.1535760, "lon": 10.1354917 }, + { "lat": 56.1535412, "lon": 10.1355015 }, + { "lat": 56.1535344, "lon": 10.1354231 }, + { "lat": 56.1535693, "lon": 10.1354133 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557531331, + "bounds": { + "minlat": 56.1535024, + "minlon": 10.1354231, + "maxlat": 56.1535412, + "maxlon": 10.1355101 + }, + "geometry": [ + { "lat": 56.1535412, "lon": 10.1355015 }, + { "lat": 56.1535091, "lon": 10.1355101 }, + { "lat": 56.1535024, "lon": 10.1354323 }, + { "lat": 56.1535344, "lon": 10.1354231 }, + { "lat": 56.1535412, "lon": 10.1355015 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557692077, + "bounds": { + "minlat": 56.1287719, + "minlon": 10.1606941, + "maxlat": 56.1288325, + "maxlon": 10.1608061 + }, + "geometry": [ + { "lat": 56.1288325, "lon": 10.1607662 }, + { "lat": 56.1287973, "lon": 10.1606941 }, + { "lat": 56.1287719, "lon": 10.1607339 }, + { "lat": 56.1288071, "lon": 10.1608061 }, + { "lat": 56.1288325, "lon": 10.1607662 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557694261, + "bounds": { + "minlat": 56.1297186, + "minlon": 10.1613185, + "maxlat": 56.1297971, + "maxlon": 10.1613821 + }, + "geometry": [ + { "lat": 56.1297186, "lon": 10.1613718 }, + { "lat": 56.1297556, "lon": 10.1613185 }, + { "lat": 56.1297971, "lon": 10.1613296 }, + { "lat": 56.1297587, "lon": 10.1613821 }, + { "lat": 56.1297186, "lon": 10.1613718 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557694262, + "bounds": { + "minlat": 56.1300240, + "minlon": 10.1603511, + "maxlat": 56.1300782, + "maxlon": 10.1604533 + }, + "geometry": [ + { "lat": 56.1300240, "lon": 10.1603864 }, + { "lat": 56.1300518, "lon": 10.1604533 }, + { "lat": 56.1300782, "lon": 10.1604179 }, + { "lat": 56.1300504, "lon": 10.1603511 }, + { "lat": 56.1300240, "lon": 10.1603864 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557694263, + "bounds": { + "minlat": 56.1300695, + "minlon": 10.1608180, + "maxlat": 56.1301516, + "maxlon": 10.1608797 + }, + "geometry": [ + { "lat": 56.1300695, "lon": 10.1608698 }, + { "lat": 56.1301179, "lon": 10.1608797 }, + { "lat": 56.1301516, "lon": 10.1608336 }, + { "lat": 56.1301057, "lon": 10.1608180 }, + { "lat": 56.1300695, "lon": 10.1608698 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557695362, + "bounds": { + "minlat": 56.1212476, + "minlon": 10.1607054, + "maxlat": 56.1213010, + "maxlon": 10.1607938 + }, + "geometry": [ + { "lat": 56.1212476, "lon": 10.1607559 }, + { "lat": 56.1212853, "lon": 10.1607938 }, + { "lat": 56.1213010, "lon": 10.1607432 }, + { "lat": 56.1212634, "lon": 10.1607054 }, + { "lat": 56.1212476, "lon": 10.1607559 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557695363, + "bounds": { + "minlat": 56.1212634, + "minlon": 10.1606504, + "maxlat": 56.1213182, + "maxlon": 10.1607432 + }, + "geometry": [ + { "lat": 56.1213010, "lon": 10.1607432 }, + { "lat": 56.1213182, "lon": 10.1606878 }, + { "lat": 56.1212803, "lon": 10.1606504 }, + { "lat": 56.1212634, "lon": 10.1607054 }, + { "lat": 56.1213010, "lon": 10.1607432 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557697252, + "bounds": { + "minlat": 56.1135657, + "minlon": 10.1736741, + "maxlat": 56.1136096, + "maxlon": 10.1737630 + }, + "geometry": [ + { "lat": 56.1135767, "lon": 10.1737630 }, + { "lat": 56.1136096, "lon": 10.1737470 }, + { "lat": 56.1135985, "lon": 10.1736741 }, + { "lat": 56.1135657, "lon": 10.1736901 }, + { "lat": 56.1135767, "lon": 10.1737630 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557697253, + "bounds": { + "minlat": 56.1136163, + "minlon": 10.1736490, + "maxlat": 56.1136604, + "maxlon": 10.1737382 + }, + "geometry": [ + { "lat": 56.1136274, "lon": 10.1737382 }, + { "lat": 56.1136604, "lon": 10.1737220 }, + { "lat": 56.1136492, "lon": 10.1736490 }, + { "lat": 56.1136163, "lon": 10.1736652 }, + { "lat": 56.1136274, "lon": 10.1737382 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557697625, + "bounds": { + "minlat": 56.1193538, + "minlon": 10.1807630, + "maxlat": 56.1194000, + "maxlon": 10.1808420 + }, + "geometry": [ + { "lat": 56.1193550, "lon": 10.1808420 }, + { "lat": 56.1194000, "lon": 10.1808329 }, + { "lat": 56.1193991, "lon": 10.1808249 }, + { "lat": 56.1194000, "lon": 10.1807729 }, + { "lat": 56.1193545, "lon": 10.1807630 }, + { "lat": 56.1193538, "lon": 10.1808046 }, + { "lat": 56.1193550, "lon": 10.1808420 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557697626, + "bounds": { + "minlat": 56.1193550, + "minlon": 10.1808329, + "maxlat": 56.1194077, + "maxlon": 10.1809200 + }, + "geometry": [ + { "lat": 56.1194000, "lon": 10.1808329 }, + { "lat": 56.1194050, "lon": 10.1808759 }, + { "lat": 56.1194077, "lon": 10.1808864 }, + { "lat": 56.1193663, "lon": 10.1809200 }, + { "lat": 56.1193594, "lon": 10.1808814 }, + { "lat": 56.1193550, "lon": 10.1808420 }, + { "lat": 56.1194000, "lon": 10.1808329 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557697627, + "bounds": { + "minlat": 56.1193769, + "minlon": 10.1809231, + "maxlat": 56.1194317, + "maxlon": 10.1810099 + }, + "geometry": [ + { "lat": 56.1194172, "lon": 10.1809231 }, + { "lat": 56.1193769, "lon": 10.1809612 }, + { "lat": 56.1193974, "lon": 10.1810099 }, + { "lat": 56.1194317, "lon": 10.1809562 }, + { "lat": 56.1194172, "lon": 10.1809231 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557697628, + "bounds": { + "minlat": 56.1190393, + "minlon": 10.1806951, + "maxlat": 56.1191158, + "maxlon": 10.1807858 + }, + "geometry": [ + { "lat": 56.1190393, "lon": 10.1807850 }, + { "lat": 56.1190684, "lon": 10.1807844 }, + { "lat": 56.1191123, "lon": 10.1807858 }, + { "lat": 56.1191158, "lon": 10.1807109 }, + { "lat": 56.1190434, "lon": 10.1806951 }, + { "lat": 56.1190393, "lon": 10.1807850 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557698974, + "bounds": { + "minlat": 56.1090227, + "minlon": 10.1560589, + "maxlat": 56.1090716, + "maxlon": 10.1561528 + }, + "geometry": [ + { "lat": 56.1090227, "lon": 10.1561231 }, + { "lat": 56.1090505, "lon": 10.1560589 }, + { "lat": 56.1090716, "lon": 10.1560923 }, + { "lat": 56.1090446, "lon": 10.1561528 }, + { "lat": 56.1090227, "lon": 10.1561231 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557698975, + "bounds": { + "minlat": 56.1090446, + "minlon": 10.1560923, + "maxlat": 56.1090958, + "maxlon": 10.1561875 + }, + "geometry": [ + { "lat": 56.1090716, "lon": 10.1560923 }, + { "lat": 56.1090958, "lon": 10.1561270 }, + { "lat": 56.1090878, "lon": 10.1561447 }, + { "lat": 56.1090687, "lon": 10.1561875 }, + { "lat": 56.1090446, "lon": 10.1561528 }, + { "lat": 56.1090716, "lon": 10.1560923 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557728505, + "bounds": { + "minlat": 56.1383872, + "minlon": 10.1137685, + "maxlat": 56.1384383, + "maxlon": 10.1138649 + }, + "geometry": [ + { "lat": 56.1383872, "lon": 10.1138649 }, + { "lat": 56.1384211, "lon": 10.1138437 }, + { "lat": 56.1384383, "lon": 10.1137685 }, + { "lat": 56.1384036, "lon": 10.1137922 }, + { "lat": 56.1383872, "lon": 10.1138649 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557728506, + "bounds": { + "minlat": 56.1384036, + "minlon": 10.1136960, + "maxlat": 56.1384548, + "maxlon": 10.1137922 + }, + "geometry": [ + { "lat": 56.1384036, "lon": 10.1137922 }, + { "lat": 56.1384210, "lon": 10.1137175 }, + { "lat": 56.1384548, "lon": 10.1136960 }, + { "lat": 56.1384383, "lon": 10.1137685 }, + { "lat": 56.1384036, "lon": 10.1137922 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557791015, + "bounds": { + "minlat": 56.1872195, + "minlon": 10.1725058, + "maxlat": 56.1872867, + "maxlon": 10.1725793 + }, + "geometry": [ + { "lat": 56.1872320, "lon": 10.1725677 }, + { "lat": 56.1872867, "lon": 10.1725793 }, + { "lat": 56.1872718, "lon": 10.1725131 }, + { "lat": 56.1872195, "lon": 10.1725058 }, + { "lat": 56.1872320, "lon": 10.1725677 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557791016, + "bounds": { + "minlat": 56.1872320, + "minlon": 10.1725677, + "maxlat": 56.1873007, + "maxlon": 10.1726412 + }, + "geometry": [ + { "lat": 56.1872320, "lon": 10.1725677 }, + { "lat": 56.1872464, "lon": 10.1726320 }, + { "lat": 56.1873007, "lon": 10.1726412 }, + { "lat": 56.1872867, "lon": 10.1725793 }, + { "lat": 56.1872320, "lon": 10.1725677 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557791640, + "bounds": { + "minlat": 56.1898151, + "minlon": 10.1806955, + "maxlat": 56.1898655, + "maxlon": 10.1807658 + }, + "geometry": [ + { "lat": 56.1898151, "lon": 10.1807481 }, + { "lat": 56.1898596, "lon": 10.1807658 }, + { "lat": 56.1898655, "lon": 10.1807126 }, + { "lat": 56.1898219, "lon": 10.1806955 }, + { "lat": 56.1898151, "lon": 10.1807481 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795209, + "bounds": { + "minlat": 56.1948429, + "minlon": 10.1703783, + "maxlat": 56.1948840, + "maxlon": 10.1704740 + }, + "geometry": [ + { "lat": 56.1948582, "lon": 10.1704740 }, + { "lat": 56.1948429, "lon": 10.1704026 }, + { "lat": 56.1948639, "lon": 10.1703783 }, + { "lat": 56.1948840, "lon": 10.1704477 }, + { "lat": 56.1948582, "lon": 10.1704740 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795210, + "bounds": { + "minlat": 56.1948051, + "minlon": 10.1701861, + "maxlat": 56.1948504, + "maxlon": 10.1702902 + }, + "geometry": [ + { "lat": 56.1948223, "lon": 10.1701861 }, + { "lat": 56.1948504, "lon": 10.1702575 }, + { "lat": 56.1948242, "lon": 10.1702902 }, + { "lat": 56.1948051, "lon": 10.1702161 }, + { "lat": 56.1948223, "lon": 10.1701861 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795475, + "bounds": { + "minlat": 56.1920193, + "minlon": 10.1649168, + "maxlat": 56.1920794, + "maxlon": 10.1650229 + }, + "geometry": [ + { "lat": 56.1920432, "lon": 10.1650229 }, + { "lat": 56.1920794, "lon": 10.1649823 }, + { "lat": 56.1920568, "lon": 10.1649168 }, + { "lat": 56.1920193, "lon": 10.1649593 }, + { "lat": 56.1920432, "lon": 10.1650229 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795956, + "bounds": { + "minlat": 56.1942807, + "minlon": 10.1646784, + "maxlat": 56.1943541, + "maxlon": 10.1648268 + }, + "geometry": [ + { "lat": 56.1942807, "lon": 10.1647870 }, + { "lat": 56.1943209, "lon": 10.1646784 }, + { "lat": 56.1943541, "lon": 10.1647179 }, + { "lat": 56.1943139, "lon": 10.1648268 }, + { "lat": 56.1942807, "lon": 10.1647870 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795957, + "bounds": { + "minlat": 56.1943139, + "minlon": 10.1647179, + "maxlat": 56.1943887, + "maxlon": 10.1648679 + }, + "geometry": [ + { "lat": 56.1943541, "lon": 10.1647179 }, + { "lat": 56.1943887, "lon": 10.1647592 }, + { "lat": 56.1943733, "lon": 10.1648026 }, + { "lat": 56.1943488, "lon": 10.1648679 }, + { "lat": 56.1943139, "lon": 10.1648268 }, + { "lat": 56.1943541, "lon": 10.1647179 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795958, + "bounds": { + "minlat": 56.1943488, + "minlon": 10.1648026, + "maxlat": 56.1943993, + "maxlon": 10.1648985 + }, + "geometry": [ + { "lat": 56.1943733, "lon": 10.1648026 }, + { "lat": 56.1943993, "lon": 10.1648356 }, + { "lat": 56.1943764, "lon": 10.1648985 }, + { "lat": 56.1943488, "lon": 10.1648679 }, + { "lat": 56.1943733, "lon": 10.1648026 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795959, + "bounds": { + "minlat": 56.1943764, + "minlon": 10.1648356, + "maxlat": 56.1944247, + "maxlon": 10.1649285 + }, + "geometry": [ + { "lat": 56.1943993, "lon": 10.1648356 }, + { "lat": 56.1944247, "lon": 10.1648642 }, + { "lat": 56.1944021, "lon": 10.1649285 }, + { "lat": 56.1943764, "lon": 10.1648985 }, + { "lat": 56.1943993, "lon": 10.1648356 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795960, + "bounds": { + "minlat": 56.1944021, + "minlon": 10.1648642, + "maxlat": 56.1944510, + "maxlon": 10.1649591 + }, + "geometry": [ + { "lat": 56.1944247, "lon": 10.1648642 }, + { "lat": 56.1944510, "lon": 10.1648930 }, + { "lat": 56.1944266, "lon": 10.1649591 }, + { "lat": 56.1944021, "lon": 10.1649285 }, + { "lat": 56.1944247, "lon": 10.1648642 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557795961, + "bounds": { + "minlat": 56.1944266, + "minlon": 10.1648930, + "maxlat": 56.1944778, + "maxlon": 10.1649912 + }, + "geometry": [ + { "lat": 56.1944510, "lon": 10.1648930 }, + { "lat": 56.1944778, "lon": 10.1649251 }, + { "lat": 56.1944535, "lon": 10.1649912 }, + { "lat": 56.1944266, "lon": 10.1649591 }, + { "lat": 56.1944510, "lon": 10.1648930 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557796105, + "bounds": { + "minlat": 56.1866183, + "minlon": 10.1681097, + "maxlat": 56.1866765, + "maxlon": 10.1682147 + }, + "geometry": [ + { "lat": 56.1866412, "lon": 10.1681097 }, + { "lat": 56.1866765, "lon": 10.1681743 }, + { "lat": 56.1866536, "lon": 10.1682147 }, + { "lat": 56.1866183, "lon": 10.1681501 }, + { "lat": 56.1866412, "lon": 10.1681097 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557796348, + "bounds": { + "minlat": 56.1689549, + "minlon": 10.1594770, + "maxlat": 56.1689974, + "maxlon": 10.1595652 + }, + "geometry": [ + { "lat": 56.1689720, "lon": 10.1595652 }, + { "lat": 56.1689549, "lon": 10.1595444 }, + { "lat": 56.1689803, "lon": 10.1594770 }, + { "lat": 56.1689974, "lon": 10.1594978 }, + { "lat": 56.1689720, "lon": 10.1595652 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557797532, + "bounds": { + "minlat": 56.1880676, + "minlon": 10.1098777, + "maxlat": 56.1881162, + "maxlon": 10.1099493 + }, + "geometry": [ + { "lat": 56.1880676, "lon": 10.1098873 }, + { "lat": 56.1881118, "lon": 10.1098777 }, + { "lat": 56.1881162, "lon": 10.1099437 }, + { "lat": 56.1880902, "lon": 10.1099493 }, + { "lat": 56.1880699, "lon": 10.1099218 }, + { "lat": 56.1880676, "lon": 10.1098873 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798490, + "bounds": { + "minlat": 56.1532618, + "minlon": 10.1166632, + "maxlat": 56.1533096, + "maxlon": 10.1167357 + }, + "geometry": [ + { "lat": 56.1532618, "lon": 10.1166763 }, + { "lat": 56.1533039, "lon": 10.1166632 }, + { "lat": 56.1533096, "lon": 10.1167227 }, + { "lat": 56.1532676, "lon": 10.1167357 }, + { "lat": 56.1532618, "lon": 10.1166763 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798491, + "bounds": { + "minlat": 56.1532676, + "minlon": 10.1167227, + "maxlat": 56.1533152, + "maxlon": 10.1167947 + }, + "geometry": [ + { "lat": 56.1533096, "lon": 10.1167227 }, + { "lat": 56.1533152, "lon": 10.1167816 }, + { "lat": 56.1532733, "lon": 10.1167947 }, + { "lat": 56.1532676, "lon": 10.1167357 }, + { "lat": 56.1533096, "lon": 10.1167227 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798492, + "bounds": { + "minlat": 56.1532733, + "minlon": 10.1167816, + "maxlat": 56.1533209, + "maxlon": 10.1168532 + }, + "geometry": [ + { "lat": 56.1533152, "lon": 10.1167816 }, + { "lat": 56.1533209, "lon": 10.1168395 }, + { "lat": 56.1532791, "lon": 10.1168532 }, + { "lat": 56.1532733, "lon": 10.1167947 }, + { "lat": 56.1533152, "lon": 10.1167816 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798493, + "bounds": { + "minlat": 56.1531351, + "minlon": 10.1180861, + "maxlat": 56.1531804, + "maxlon": 10.1181516 + }, + "geometry": [ + { "lat": 56.1531804, "lon": 10.1180931 }, + { "lat": 56.1531773, "lon": 10.1181516 }, + { "lat": 56.1531351, "lon": 10.1181450 }, + { "lat": 56.1531379, "lon": 10.1180861 }, + { "lat": 56.1531804, "lon": 10.1180931 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798494, + "bounds": { + "minlat": 56.1531319, + "minlon": 10.1181450, + "maxlat": 56.1531773, + "maxlon": 10.1182114 + }, + "geometry": [ + { "lat": 56.1531773, "lon": 10.1181516 }, + { "lat": 56.1531741, "lon": 10.1182114 }, + { "lat": 56.1531319, "lon": 10.1182036 }, + { "lat": 56.1531351, "lon": 10.1181450 }, + { "lat": 56.1531773, "lon": 10.1181516 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798495, + "bounds": { + "minlat": 56.1531297, + "minlon": 10.1182036, + "maxlat": 56.1531741, + "maxlon": 10.1182515 + }, + "geometry": [ + { "lat": 56.1531741, "lon": 10.1182114 }, + { "lat": 56.1531719, "lon": 10.1182515 }, + { "lat": 56.1531297, "lon": 10.1182438 }, + { "lat": 56.1531319, "lon": 10.1182036 }, + { "lat": 56.1531741, "lon": 10.1182114 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798496, + "bounds": { + "minlat": 56.1531269, + "minlon": 10.1182438, + "maxlat": 56.1531719, + "maxlon": 10.1183021 + }, + "geometry": [ + { "lat": 56.1531719, "lon": 10.1182515 }, + { "lat": 56.1531692, "lon": 10.1183021 }, + { "lat": 56.1531269, "lon": 10.1182965 }, + { "lat": 56.1531297, "lon": 10.1182438 }, + { "lat": 56.1531719, "lon": 10.1182515 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557798734, + "bounds": { + "minlat": 56.1302598, + "minlon": 10.1147985, + "maxlat": 56.1302860, + "maxlon": 10.1148804 + }, + "geometry": [ + { "lat": 56.1302598, "lon": 10.1148775 }, + { "lat": 56.1302830, "lon": 10.1148804 }, + { "lat": 56.1302860, "lon": 10.1148014 }, + { "lat": 56.1302629, "lon": 10.1147985 }, + { "lat": 56.1302598, "lon": 10.1148775 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557799603, + "bounds": { + "minlat": 56.1285953, + "minlon": 10.1580423, + "maxlat": 56.1286770, + "maxlon": 10.1581975 + }, + "geometry": [ + { "lat": 56.1285953, "lon": 10.1580887 }, + { "lat": 56.1286263, "lon": 10.1580423 }, + { "lat": 56.1286770, "lon": 10.1581511 }, + { "lat": 56.1286460, "lon": 10.1581975 }, + { "lat": 56.1285953, "lon": 10.1580887 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557799604, + "bounds": { + "minlat": 56.1286882, + "minlon": 10.1579437, + "maxlat": 56.1287380, + "maxlon": 10.1580320 + }, + "geometry": [ + { "lat": 56.1286882, "lon": 10.1579868 }, + { "lat": 56.1287093, "lon": 10.1580320 }, + { "lat": 56.1287380, "lon": 10.1579889 }, + { "lat": 56.1287168, "lon": 10.1579437 }, + { "lat": 56.1286882, "lon": 10.1579868 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557840753, + "bounds": { + "minlat": 56.0960764, + "minlon": 10.1359120, + "maxlat": 56.0961285, + "maxlon": 10.1360112 + }, + "geometry": [ + { "lat": 56.0960764, "lon": 10.1359442 }, + { "lat": 56.0961000, "lon": 10.1360112 }, + { "lat": 56.0961285, "lon": 10.1359790 }, + { "lat": 56.0961048, "lon": 10.1359120 }, + { "lat": 56.0960764, "lon": 10.1359442 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557840754, + "bounds": { + "minlat": 56.0963753, + "minlon": 10.1355622, + "maxlat": 56.0964427, + "maxlon": 10.1356654 + }, + "geometry": [ + { "lat": 56.0964427, "lon": 10.1356096 }, + { "lat": 56.0963913, "lon": 10.1356654 }, + { "lat": 56.0963753, "lon": 10.1356180 }, + { "lat": 56.0964267, "lon": 10.1355622 }, + { "lat": 56.0964427, "lon": 10.1356096 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "2", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557845987, + "bounds": { + "minlat": 56.1670137, + "minlon": 10.2254496, + "maxlat": 56.1670946, + "maxlon": 10.2255803 + }, + "geometry": [ + { "lat": 56.1670768, "lon": 10.2254496 }, + { "lat": 56.1670946, "lon": 10.2255091 }, + { "lat": 56.1670322, "lon": 10.2255803 }, + { "lat": 56.1670137, "lon": 10.2255241 }, + { "lat": 56.1670768, "lon": 10.2254496 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557845988, + "bounds": { + "minlat": 56.1669549, + "minlon": 10.2255241, + "maxlat": 56.1670322, + "maxlon": 10.2256517 + }, + "geometry": [ + { "lat": 56.1670137, "lon": 10.2255241 }, + { "lat": 56.1669549, "lon": 10.2255935 }, + { "lat": 56.1669727, "lon": 10.2256517 }, + { "lat": 56.1670322, "lon": 10.2255803 }, + { "lat": 56.1670137, "lon": 10.2255241 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557845989, + "bounds": { + "minlat": 56.1668949, + "minlon": 10.2255935, + "maxlat": 56.1669727, + "maxlon": 10.2257215 + }, + "geometry": [ + { "lat": 56.1669549, "lon": 10.2255935 }, + { "lat": 56.1668949, "lon": 10.2256642 }, + { "lat": 56.1669131, "lon": 10.2257215 }, + { "lat": 56.1669727, "lon": 10.2256517 }, + { "lat": 56.1669549, "lon": 10.2255935 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557845990, + "bounds": { + "minlat": 56.1668299, + "minlon": 10.2256642, + "maxlat": 56.1669131, + "maxlon": 10.2257971 + }, + "geometry": [ + { "lat": 56.1668949, "lon": 10.2256642 }, + { "lat": 56.1668299, "lon": 10.2257409 }, + { "lat": 56.1668485, "lon": 10.2257971 }, + { "lat": 56.1669131, "lon": 10.2257215 }, + { "lat": 56.1668949, "lon": 10.2256642 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557864914, + "bounds": { + "minlat": 56.1423242, + "minlon": 10.2025469, + "maxlat": 56.1423680, + "maxlon": 10.2026303 + }, + "geometry": [ + { "lat": 56.1423242, "lon": 10.2025742 }, + { "lat": 56.1423481, "lon": 10.2025469 }, + { "lat": 56.1423680, "lon": 10.2026030 }, + { "lat": 56.1423441, "lon": 10.2026303 }, + { "lat": 56.1423242, "lon": 10.2025742 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1557875078, + "bounds": { + "minlat": 56.1743782, + "minlon": 10.2179861, + "maxlat": 56.1744125, + "maxlon": 10.2181069 + }, + "geometry": [ + { "lat": 56.1743782, "lon": 10.2180985 }, + { "lat": 56.1743971, "lon": 10.2181069 }, + { "lat": 56.1744125, "lon": 10.2179957 }, + { "lat": 56.1743937, "lon": 10.2179861 }, + { "lat": 56.1743782, "lon": 10.2180985 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557875079, + "bounds": { + "minlat": 56.1743937, + "minlon": 10.2178945, + "maxlat": 56.1744253, + "maxlon": 10.2179957 + }, + "geometry": [ + { "lat": 56.1744125, "lon": 10.2179957 }, + { "lat": 56.1744253, "lon": 10.2179029 }, + { "lat": 56.1744063, "lon": 10.2178945 }, + { "lat": 56.1743937, "lon": 10.2179861 }, + { "lat": 56.1744125, "lon": 10.2179957 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557914994, + "bounds": { + "minlat": 56.1688988, + "minlon": 10.1960691, + "maxlat": 56.1689601, + "maxlon": 10.1961674 + }, + "geometry": [ + { "lat": 56.1689420, "lon": 10.1960691 }, + { "lat": 56.1689601, "lon": 10.1961184 }, + { "lat": 56.1689160, "lon": 10.1961674 }, + { "lat": 56.1688988, "lon": 10.1961188 }, + { "lat": 56.1689420, "lon": 10.1960691 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557915187, + "bounds": { + "minlat": 56.1693807, + "minlon": 10.1975831, + "maxlat": 56.1694309, + "maxlon": 10.1976667 + }, + "geometry": [ + { "lat": 56.1694150, "lon": 10.1975831 }, + { "lat": 56.1694309, "lon": 10.1976334 }, + { "lat": 56.1693956, "lon": 10.1976667 }, + { "lat": 56.1693807, "lon": 10.1976174 }, + { "lat": 56.1694150, "lon": 10.1975831 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557957578, + "bounds": { + "minlat": 56.1329284, + "minlon": 10.1953992, + "maxlat": 56.1329779, + "maxlon": 10.1954980 + }, + "geometry": [ + { "lat": 56.1329469, "lon": 10.1954980 }, + { "lat": 56.1329779, "lon": 10.1954731 }, + { "lat": 56.1329594, "lon": 10.1953992 }, + { "lat": 56.1329284, "lon": 10.1954242 }, + { "lat": 56.1329469, "lon": 10.1954980 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557957579, + "bounds": { + "minlat": 56.1329594, + "minlon": 10.1953792, + "maxlat": 56.1330025, + "maxlon": 10.1954731 + }, + "geometry": [ + { "lat": 56.1329779, "lon": 10.1954731 }, + { "lat": 56.1330025, "lon": 10.1954534 }, + { "lat": 56.1329840, "lon": 10.1953792 }, + { "lat": 56.1329594, "lon": 10.1953992 }, + { "lat": 56.1329779, "lon": 10.1954731 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557957580, + "bounds": { + "minlat": 56.1329840, + "minlon": 10.1953563, + "maxlat": 56.1330313, + "maxlon": 10.1954534 + }, + "geometry": [ + { "lat": 56.1330025, "lon": 10.1954534 }, + { "lat": 56.1330313, "lon": 10.1954302 }, + { "lat": 56.1330122, "lon": 10.1953563 }, + { "lat": 56.1329840, "lon": 10.1953792 }, + { "lat": 56.1330025, "lon": 10.1954534 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557957581, + "bounds": { + "minlat": 56.1330122, + "minlon": 10.1953335, + "maxlat": 56.1330587, + "maxlon": 10.1954302 + }, + "geometry": [ + { "lat": 56.1330313, "lon": 10.1954302 }, + { "lat": 56.1330587, "lon": 10.1954077 }, + { "lat": 56.1330396, "lon": 10.1953335 }, + { "lat": 56.1330122, "lon": 10.1953563 }, + { "lat": 56.1330313, "lon": 10.1954302 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557957582, + "bounds": { + "minlat": 56.1330396, + "minlon": 10.1953112, + "maxlat": 56.1330853, + "maxlon": 10.1954077 + }, + "geometry": [ + { "lat": 56.1330587, "lon": 10.1954077 }, + { "lat": 56.1330853, "lon": 10.1953859 }, + { "lat": 56.1330664, "lon": 10.1953112 }, + { "lat": 56.1330396, "lon": 10.1953335 }, + { "lat": 56.1330587, "lon": 10.1954077 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557958747, + "bounds": { + "minlat": 56.1071747, + "minlon": 10.2057512, + "maxlat": 56.1072075, + "maxlon": 10.2058488 + }, + "geometry": [ + { "lat": 56.1072075, "lon": 10.2058474 }, + { "lat": 56.1072065, "lon": 10.2057512 }, + { "lat": 56.1071747, "lon": 10.2057519 }, + { "lat": 56.1071760, "lon": 10.2058488 }, + { "lat": 56.1072075, "lon": 10.2058474 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1557958748, + "bounds": { + "minlat": 56.1072065, + "minlon": 10.2057501, + "maxlat": 56.1072419, + "maxlon": 10.2058474 + }, + "geometry": [ + { "lat": 56.1072075, "lon": 10.2058474 }, + { "lat": 56.1072419, "lon": 10.2058463 }, + { "lat": 56.1072409, "lon": 10.2057501 }, + { "lat": 56.1072065, "lon": 10.2057512 }, + { "lat": 56.1072075, "lon": 10.2058474 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558044753, + "bounds": { + "minlat": 56.1071313, + "minlon": 10.1024018, + "maxlat": 56.1071681, + "maxlon": 10.1024946 + }, + "geometry": [ + { "lat": 56.1071335, "lon": 10.1024018 }, + { "lat": 56.1071681, "lon": 10.1024045 }, + { "lat": 56.1071659, "lon": 10.1024946 }, + { "lat": 56.1071313, "lon": 10.1024919 }, + { "lat": 56.1071335, "lon": 10.1024018 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558044906, + "bounds": { + "minlat": 56.1093384, + "minlon": 10.0780375, + "maxlat": 56.1093836, + "maxlon": 10.0781198 + }, + "geometry": [ + { "lat": 56.1093808, "lon": 10.0780375 }, + { "lat": 56.1093384, "lon": 10.0780424 }, + { "lat": 56.1093411, "lon": 10.0781198 }, + { "lat": 56.1093836, "lon": 10.0781149 }, + { "lat": 56.1093808, "lon": 10.0780375 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558065357, + "bounds": { + "minlat": 56.2109010, + "minlon": 10.2844079, + "maxlat": 56.2109587, + "maxlon": 10.2845038 + }, + "geometry": [ + { "lat": 56.2109010, "lon": 10.2844582 }, + { "lat": 56.2109405, "lon": 10.2845038 }, + { "lat": 56.2109587, "lon": 10.2844561 }, + { "lat": 56.2109185, "lon": 10.2844079 }, + { "lat": 56.2109010, "lon": 10.2844582 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558065358, + "bounds": { + "minlat": 56.2109185, + "minlon": 10.2843627, + "maxlat": 56.2109755, + "maxlon": 10.2844561 + }, + "geometry": [ + { "lat": 56.2109185, "lon": 10.2844079 }, + { "lat": 56.2109353, "lon": 10.2843627 }, + { "lat": 56.2109755, "lon": 10.2844108 }, + { "lat": 56.2109587, "lon": 10.2844561 }, + { "lat": 56.2109185, "lon": 10.2844079 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558065900, + "bounds": { + "minlat": 56.2364416, + "minlon": 10.2337784, + "maxlat": 56.2364957, + "maxlon": 10.2338602 + }, + "geometry": [ + { "lat": 56.2364844, "lon": 10.2337784 }, + { "lat": 56.2364957, "lon": 10.2338295 }, + { "lat": 56.2364529, "lon": 10.2338602 }, + { "lat": 56.2364416, "lon": 10.2338090 }, + { "lat": 56.2364844, "lon": 10.2337784 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558065901, + "bounds": { + "minlat": 56.2364529, + "minlon": 10.2338295, + "maxlat": 56.2365069, + "maxlon": 10.2339116 + }, + "geometry": [ + { "lat": 56.2364957, "lon": 10.2338295 }, + { "lat": 56.2365069, "lon": 10.2338806 }, + { "lat": 56.2364645, "lon": 10.2339116 }, + { "lat": 56.2364529, "lon": 10.2338602 }, + { "lat": 56.2364957, "lon": 10.2338295 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558065902, + "bounds": { + "minlat": 56.2363461, + "minlon": 10.2338495, + "maxlat": 56.2363963, + "maxlon": 10.2339256 + }, + "geometry": [ + { "lat": 56.2363858, "lon": 10.2338495 }, + { "lat": 56.2363963, "lon": 10.2338976 }, + { "lat": 56.2363566, "lon": 10.2339256 }, + { "lat": 56.2363461, "lon": 10.2338775 }, + { "lat": 56.2363858, "lon": 10.2338495 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558065903, + "bounds": { + "minlat": 56.2363566, + "minlon": 10.2338976, + "maxlat": 56.2364071, + "maxlon": 10.2339760 + }, + "geometry": [ + { "lat": 56.2363963, "lon": 10.2338976 }, + { "lat": 56.2364071, "lon": 10.2339476 }, + { "lat": 56.2363680, "lon": 10.2339760 }, + { "lat": 56.2363566, "lon": 10.2339256 }, + { "lat": 56.2363963, "lon": 10.2338976 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558065904, + "bounds": { + "minlat": 56.2357551, + "minlon": 10.2342611, + "maxlat": 56.2358113, + "maxlon": 10.2343743 + }, + "geometry": [ + { "lat": 56.2357551, "lon": 10.2342888 }, + { "lat": 56.2357774, "lon": 10.2343743 }, + { "lat": 56.2358113, "lon": 10.2343265 }, + { "lat": 56.2357956, "lon": 10.2342611 }, + { "lat": 56.2357551, "lon": 10.2342888 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558073757, + "bounds": { + "minlat": 56.1783737, + "minlon": 10.2011057, + "maxlat": 56.1784244, + "maxlon": 10.2011837 + }, + "geometry": [ + { "lat": 56.1784145, "lon": 10.2011057 }, + { "lat": 56.1784244, "lon": 10.2011599 }, + { "lat": 56.1783835, "lon": 10.2011837 }, + { "lat": 56.1783737, "lon": 10.2011286 }, + { "lat": 56.1784145, "lon": 10.2011057 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558073758, + "bounds": { + "minlat": 56.1783835, + "minlon": 10.2011599, + "maxlat": 56.1784342, + "maxlon": 10.2012366 + }, + "geometry": [ + { "lat": 56.1784244, "lon": 10.2011599 }, + { "lat": 56.1784342, "lon": 10.2012138 }, + { "lat": 56.1783929, "lon": 10.2012366 }, + { "lat": 56.1783835, "lon": 10.2011837 }, + { "lat": 56.1784244, "lon": 10.2011599 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558073920, + "bounds": { + "minlat": 56.1789240, + "minlon": 10.2005140, + "maxlat": 56.1789814, + "maxlon": 10.2005993 + }, + "geometry": [ + { "lat": 56.1789240, "lon": 10.2005457 }, + { "lat": 56.1789699, "lon": 10.2005140 }, + { "lat": 56.1789814, "lon": 10.2005675 }, + { "lat": 56.1789354, "lon": 10.2005993 }, + { "lat": 56.1789240, "lon": 10.2005457 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558075046, + "bounds": { + "minlat": 56.1895097, + "minlon": 10.2151869, + "maxlat": 56.1895423, + "maxlon": 10.2152598 + }, + "geometry": [ + { "lat": 56.1895423, "lon": 10.2151967 }, + { "lat": 56.1895174, "lon": 10.2151869 }, + { "lat": 56.1895097, "lon": 10.2152501 }, + { "lat": 56.1895346, "lon": 10.2152598 }, + { "lat": 56.1895423, "lon": 10.2151967 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558075198, + "bounds": { + "minlat": 56.1870938, + "minlon": 10.2150086, + "maxlat": 56.1871554, + "maxlon": 10.2151406 + }, + "geometry": [ + { "lat": 56.1870938, "lon": 10.2150397 }, + { "lat": 56.1871277, "lon": 10.2151406 }, + { "lat": 56.1871554, "lon": 10.2151103 }, + { "lat": 56.1871214, "lon": 10.2150086 }, + { "lat": 56.1870938, "lon": 10.2150397 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558075199, + "bounds": { + "minlat": 56.1871214, + "minlon": 10.2149803, + "maxlat": 56.1871817, + "maxlon": 10.2151103 + }, + "geometry": [ + { "lat": 56.1871554, "lon": 10.2151103 }, + { "lat": 56.1871817, "lon": 10.2150820 }, + { "lat": 56.1871477, "lon": 10.2149803 }, + { "lat": 56.1871214, "lon": 10.2150086 }, + { "lat": 56.1871554, "lon": 10.2151103 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558075497, + "bounds": { + "minlat": 56.1995654, + "minlon": 10.1825360, + "maxlat": 56.1995974, + "maxlon": 10.1826078 + }, + "geometry": [ + { "lat": 56.1995654, "lon": 10.1825390 }, + { "lat": 56.1995675, "lon": 10.1826078 }, + { "lat": 56.1995974, "lon": 10.1826048 }, + { "lat": 56.1995953, "lon": 10.1825360 }, + { "lat": 56.1995654, "lon": 10.1825390 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558096098, + "bounds": { + "minlat": 56.1294697, + "minlon": 10.2028060, + "maxlat": 56.1295195, + "maxlon": 10.2028829 + }, + "geometry": [ + { "lat": 56.1294697, "lon": 10.2028555 }, + { "lat": 56.1294805, "lon": 10.2028829 }, + { "lat": 56.1295195, "lon": 10.2028335 }, + { "lat": 56.1295087, "lon": 10.2028060 }, + { "lat": 56.1294697, "lon": 10.2028555 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1" + } +}, +{ + "type": "way", + "id": 1558138049, + "bounds": { + "minlat": 56.1435947, + "minlon": 10.1952020, + "maxlat": 56.1436445, + "maxlon": 10.1952487 + }, + "geometry": [ + { "lat": 56.1435970, "lon": 10.1952487 }, + { "lat": 56.1435947, "lon": 10.1952113 }, + { "lat": 56.1436422, "lon": 10.1952020 }, + { "lat": 56.1436445, "lon": 10.1952394 }, + { "lat": 56.1435970, "lon": 10.1952487 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1558180147, + "bounds": { + "minlat": 56.2503548, + "minlon": 10.1534300, + "maxlat": 56.2504013, + "maxlon": 10.1534931 + }, + "geometry": [ + { "lat": 56.2503573, "lon": 10.1534931 }, + { "lat": 56.2504013, "lon": 10.1534868 }, + { "lat": 56.2503988, "lon": 10.1534300 }, + { "lat": 56.2503548, "lon": 10.1534363 }, + { "lat": 56.2503573, "lon": 10.1534931 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558180148, + "bounds": { + "minlat": 56.2502586, + "minlon": 10.1534449, + "maxlat": 56.2503016, + "maxlon": 10.1535087 + }, + "geometry": [ + { "lat": 56.2502989, "lon": 10.1534449 }, + { "lat": 56.2503016, "lon": 10.1535026 }, + { "lat": 56.2502613, "lon": 10.1535087 }, + { "lat": 56.2502586, "lon": 10.1534510 }, + { "lat": 56.2502989, "lon": 10.1534449 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558180587, + "bounds": { + "minlat": 56.2193037, + "minlon": 10.1582418, + "maxlat": 56.2193540, + "maxlon": 10.1583301 + }, + "geometry": [ + { "lat": 56.2193037, "lon": 10.1582909 }, + { "lat": 56.2193348, "lon": 10.1583301 }, + { "lat": 56.2193540, "lon": 10.1582811 }, + { "lat": 56.2193228, "lon": 10.1582418 }, + { "lat": 56.2193037, "lon": 10.1582909 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558180588, + "bounds": { + "minlat": 56.2193893, + "minlon": 10.1583472, + "maxlat": 56.2194424, + "maxlon": 10.1584383 + }, + "geometry": [ + { "lat": 56.2194078, "lon": 10.1583472 }, + { "lat": 56.2194424, "lon": 10.1583922 }, + { "lat": 56.2194239, "lon": 10.1584383 }, + { "lat": 56.2193893, "lon": 10.1583933 }, + { "lat": 56.2194078, "lon": 10.1583472 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558180829, + "bounds": { + "minlat": 56.1942195, + "minlon": 10.2469410, + "maxlat": 56.1942617, + "maxlon": 10.2470349 + }, + "geometry": [ + { "lat": 56.1942275, "lon": 10.2470349 }, + { "lat": 56.1942617, "lon": 10.2470243 }, + { "lat": 56.1942537, "lon": 10.2469410 }, + { "lat": 56.1942195, "lon": 10.2469517 }, + { "lat": 56.1942275, "lon": 10.2470349 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558180978, + "bounds": { + "minlat": 56.2712786, + "minlon": 10.3092514, + "maxlat": 56.2713341, + "maxlon": 10.3093550 + }, + "geometry": [ + { "lat": 56.2713066, "lon": 10.3093550 }, + { "lat": 56.2713341, "lon": 10.3092891 }, + { "lat": 56.2713056, "lon": 10.3092514 }, + { "lat": 56.2712786, "lon": 10.3093157 }, + { "lat": 56.2713066, "lon": 10.3093550 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558180979, + "bounds": { + "minlat": 56.2712501, + "minlon": 10.3092126, + "maxlat": 56.2713056, + "maxlon": 10.3093157 + }, + "geometry": [ + { "lat": 56.2713056, "lon": 10.3092514 }, + { "lat": 56.2712772, "lon": 10.3092126 }, + { "lat": 56.2712501, "lon": 10.3092768 }, + { "lat": 56.2712786, "lon": 10.3093157 }, + { "lat": 56.2713056, "lon": 10.3092514 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558181067, + "bounds": { + "minlat": 56.2766474, + "minlon": 10.3163108, + "maxlat": 56.2767015, + "maxlon": 10.3164166 + }, + "geometry": [ + { "lat": 56.2767015, "lon": 10.3163466 }, + { "lat": 56.2766787, "lon": 10.3164166 }, + { "lat": 56.2766474, "lon": 10.3163830 }, + { "lat": 56.2766714, "lon": 10.3163108 }, + { "lat": 56.2767015, "lon": 10.3163466 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558267557, + "bounds": { + "minlat": 56.1576267, + "minlon": 10.1979978, + "maxlat": 56.1576890, + "maxlon": 10.1980504 + }, + "geometry": [ + { "lat": 56.1576267, "lon": 10.1980159 }, + { "lat": 56.1576725, "lon": 10.1979978 }, + { "lat": 56.1576890, "lon": 10.1980335 }, + { "lat": 56.1576442, "lon": 10.1980504 }, + { "lat": 56.1576267, "lon": 10.1980159 } + ], + "tags": { + "amenity": "parking", + "capacity": "1", + "capacity:disabled": "1", + "parking": "street_side" + } +}, +{ + "type": "way", + "id": 1558339580, + "bounds": { + "minlat": 56.1496634, + "minlon": 10.1994547, + "maxlat": 56.1497134, + "maxlon": 10.1995838 + }, + "geometry": [ + { "lat": 56.1497084, "lon": 10.1995100 }, + { "lat": 56.1497134, "lon": 10.1995838 }, + { "lat": 56.1496678, "lon": 10.1995328 }, + { "lat": 56.1496634, "lon": 10.1994547 }, + { "lat": 56.1497084, "lon": 10.1995100 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558339581, + "bounds": { + "minlat": 56.1496678, + "minlon": 10.1995328, + "maxlat": 56.1497190, + "maxlon": 10.1996661 + }, + "geometry": [ + { "lat": 56.1497134, "lon": 10.1995838 }, + { "lat": 56.1497190, "lon": 10.1996661 }, + { "lat": 56.1496721, "lon": 10.1996092 }, + { "lat": 56.1496678, "lon": 10.1995328 }, + { "lat": 56.1497134, "lon": 10.1995838 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558340155, + "bounds": { + "minlat": 56.1496907, + "minlon": 10.1999417, + "maxlat": 56.1497450, + "maxlon": 10.2000893 + }, + "geometry": [ + { "lat": 56.1496907, "lon": 10.1999417 }, + { "lat": 56.1497396, "lon": 10.2000044 }, + { "lat": 56.1497450, "lon": 10.2000893 }, + { "lat": 56.1496959, "lon": 10.2000348 }, + { "lat": 56.1496907, "lon": 10.1999417 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558341652, + "bounds": { + "minlat": 56.1503659, + "minlon": 10.2036038, + "maxlat": 56.1504298, + "maxlon": 10.2037311 + }, + "geometry": [ + { "lat": 56.1503659, "lon": 10.2036038 }, + { "lat": 56.1503783, "lon": 10.2036830 }, + { "lat": 56.1504298, "lon": 10.2037311 }, + { "lat": 56.1504169, "lon": 10.2036478 }, + { "lat": 56.1503659, "lon": 10.2036038 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558341653, + "bounds": { + "minlat": 56.1503783, + "minlon": 10.2036830, + "maxlat": 56.1504432, + "maxlon": 10.2038177 + }, + "geometry": [ + { "lat": 56.1503783, "lon": 10.2036830 }, + { "lat": 56.1503912, "lon": 10.2037702 }, + { "lat": 56.1504432, "lon": 10.2038177 }, + { "lat": 56.1504358, "lon": 10.2037697 }, + { "lat": 56.1504298, "lon": 10.2037311 }, + { "lat": 56.1503783, "lon": 10.2036830 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558358544, + "bounds": { + "minlat": 56.1178738, + "minlon": 10.1736079, + "maxlat": 56.1179165, + "maxlon": 10.1737081 + }, + "geometry": [ + { "lat": 56.1178738, "lon": 10.1737081 }, + { "lat": 56.1178738, "lon": 10.1736099 }, + { "lat": 56.1179157, "lon": 10.1736079 }, + { "lat": 56.1179165, "lon": 10.1737070 }, + { "lat": 56.1178738, "lon": 10.1737081 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558373177, + "bounds": { + "minlat": 56.1173788, + "minlon": 10.1526739, + "maxlat": 56.1174304, + "maxlon": 10.1527640 + }, + "geometry": [ + { "lat": 56.1173986, "lon": 10.1527640 }, + { "lat": 56.1173788, "lon": 10.1527226 }, + { "lat": 56.1174109, "lon": 10.1526739 }, + { "lat": 56.1174304, "lon": 10.1527147 }, + { "lat": 56.1173986, "lon": 10.1527640 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558373178, + "bounds": { + "minlat": 56.1173575, + "minlon": 10.1526280, + "maxlat": 56.1174109, + "maxlon": 10.1527226 + }, + "geometry": [ + { "lat": 56.1173788, "lon": 10.1527226 }, + { "lat": 56.1173575, "lon": 10.1526784 }, + { "lat": 56.1173888, "lon": 10.1526280 }, + { "lat": 56.1174109, "lon": 10.1526739 }, + { "lat": 56.1173788, "lon": 10.1527226 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558373179, + "bounds": { + "minlat": 56.1163327, + "minlon": 10.1528661, + "maxlat": 56.1163765, + "maxlon": 10.1529590 + }, + "geometry": [ + { "lat": 56.1163467, "lon": 10.1528661 }, + { "lat": 56.1163765, "lon": 10.1528828 }, + { "lat": 56.1163619, "lon": 10.1529590 }, + { "lat": 56.1163364, "lon": 10.1529431 }, + { "lat": 56.1163327, "lon": 10.1529406 }, + { "lat": 56.1163467, "lon": 10.1528661 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558377064, + "bounds": { + "minlat": 56.2104071, + "minlon": 10.2878990, + "maxlat": 56.2104476, + "maxlon": 10.2879856 + }, + "geometry": [ + { "lat": 56.2104273, "lon": 10.2879856 }, + { "lat": 56.2104476, "lon": 10.2879188 }, + { "lat": 56.2104274, "lon": 10.2878990 }, + { "lat": 56.2104071, "lon": 10.2879658 }, + { "lat": 56.2104273, "lon": 10.2879856 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558377065, + "bounds": { + "minlat": 56.2108843, + "minlon": 10.2881990, + "maxlat": 56.2109164, + "maxlon": 10.2882914 + }, + "geometry": [ + { "lat": 56.2108843, "lon": 10.2882807 }, + { "lat": 56.2108917, "lon": 10.2881990 }, + { "lat": 56.2109164, "lon": 10.2882098 }, + { "lat": 56.2109088, "lon": 10.2882914 }, + { "lat": 56.2108843, "lon": 10.2882807 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558559365, + "bounds": { + "minlat": 56.1511341, + "minlon": 10.2027002, + "maxlat": 56.1512089, + "maxlon": 10.2027739 + }, + "geometry": [ + { "lat": 56.1511383, "lon": 10.2027739 }, + { "lat": 56.1511341, "lon": 10.2027362 }, + { "lat": 56.1512037, "lon": 10.2027002 }, + { "lat": 56.1512089, "lon": 10.2027409 }, + { "lat": 56.1511383, "lon": 10.2027739 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558604131, + "bounds": { + "minlat": 56.1770521, + "minlon": 10.2306954, + "maxlat": 56.1771407, + "maxlon": 10.2307802 + }, + "geometry": [ + { "lat": 56.1771407, "lon": 10.2307426 }, + { "lat": 56.1771193, "lon": 10.2307802 }, + { "lat": 56.1770873, "lon": 10.2307637 }, + { "lat": 56.1770565, "lon": 10.2307478 }, + { "lat": 56.1770521, "lon": 10.2306954 }, + { "lat": 56.1770935, "lon": 10.2307175 }, + { "lat": 56.1771407, "lon": 10.2307426 } + ], + "tags": { + "amenity": "parking", + "capacity": "2", + "capacity:disabled": "2", + "parking": "street_side", + "surface": "gravel" + } +}, +{ + "type": "way", + "id": 1558604132, + "bounds": { + "minlat": 56.1770873, + "minlon": 10.2307175, + "maxlat": 56.1771407, + "maxlon": 10.2307802 + }, + "geometry": [ + { "lat": 56.1771407, "lon": 10.2307426 }, + { "lat": 56.1771193, "lon": 10.2307802 }, + { "lat": 56.1770873, "lon": 10.2307637 }, + { "lat": 56.1770935, "lon": 10.2307175 }, + { "lat": 56.1771407, "lon": 10.2307426 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "way", + "id": 1558604133, + "bounds": { + "minlat": 56.1770521, + "minlon": 10.2306954, + "maxlat": 56.1770935, + "maxlon": 10.2307637 + }, + "geometry": [ + { "lat": 56.1770873, "lon": 10.2307637 }, + { "lat": 56.1770565, "lon": 10.2307478 }, + { "lat": 56.1770521, "lon": 10.2306954 }, + { "lat": 56.1770935, "lon": 10.2307175 }, + { "lat": 56.1770873, "lon": 10.2307637 } + ], + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, +{ + "type": "relation", + "id": 17151325, + "bounds": { + "minlat": 56.0886708, + "minlon": 10.2477857, + "maxlat": 56.0888327, + "maxlon": 10.2480758 + }, + "tags": { + "access": "yes", + "amenity": "parking", + "capacity": "11", + "capacity:disabled": "1", + "fee": "no", + "orientation": "perpendicular", + "parking": "street_side", + "surface": "asphalt", + "type": "multipolygon" + } +} + + ] +} diff --git a/tests/resources/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap b/tests/resources/data/webkort.aarhuskommune.dk/spatialmap?mtm_spatialmaps-handicap-parking similarity index 99% rename from tests/resources/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap rename to tests/resources/data/webkort.aarhuskommune.dk/spatialmap?mtm_spatialmaps-handicap-parking index 77f0634..acf02f2 100644 --- a/tests/resources/data/webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap +++ b/tests/resources/data/webkort.aarhuskommune.dk/spatialmap?mtm_spatialmaps-handicap-parking @@ -1 +1 @@ -{ "type": "FeatureCollection","crs": {"type": "name", "properties": {"name": "EPSG:25832"}}, "bbox": [570415.7999999999,6211070.354199971,577384.3500328106,6232262.860918405],"features": [{"type": "Feature","geometry": {"type": "Point","coordinates": [575153.9524951308, 6224260.609753487]},"properties": {"vejnavn": "Domkirkeplads/Bispegade", "husnnr": "1", "invalidepladser": 6, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:23.91", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:23.91", "mi_style": null, "mi_prinx": 172}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574656.6043964146, 6223886.745852402]},"properties": {"vejnavn": "Christiansgade", "husnnr": "2", "invalidepladser": 5, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:23.943", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:23.943", "mi_style": null, "mi_prinx": 173}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574519.3323188772, 6223394.810052871]},"properties": {"vejnavn": "Krigersvej", "husnnr": "5", "invalidepladser": 4, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:04:59.57", "mi_style": null, "mi_prinx": 175}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574916.9504383737, 6223961.870560985]},"properties": {"vejnavn": "Telefonsmøgen / Busgaden", "husnnr": "6", "invalidepladser": 3, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.113", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.113", "mi_style": null, "mi_prinx": 176}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574742.8394360626, 6224182.215710367]},"properties": {"vejnavn": "Grønnegade", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.15", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.15", "mi_style": null, "mi_prinx": 177}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574727.5186573606, 6224387.563912335]},"properties": {"vejnavn": "Klostergade 1", "husnnr": "74", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.193", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.193", "mi_style": null, "mi_prinx": 178}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574855.8060164271, 6224401.099589322]},"properties": {"vejnavn": "Klostergade 2", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.26", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:04.027", "mi_style": null, "mi_prinx": 179}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574504.0892603148, 6223553.9135994455]},"properties": {"vejnavn": "Skt. Nicolaus Gade", "husnnr": "1 til 3", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.363", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.363", "mi_style": null, "mi_prinx": 181}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574952.5463860369, 6223535.318275151]},"properties": {"vejnavn": "Ny Banegårdsgade", "husnnr": "49", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.403", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:16:08.26", "mi_style": null, "mi_prinx": 182}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575228.2588088225, 6224049.9438427165]},"properties": {"vejnavn": "Kannikegade", "husnnr": "16A", "invalidepladser": 2, "bemrk": "Tidsbegrænset", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.46", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:57:11.063", "mi_style": null, "mi_prinx": 183}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575319.8423906788, 6224763.017489845]},"properties": {"vejnavn": "Nørreport", "husnnr": "22", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.51", "mi_style": null, "mi_prinx": 184}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574890.2916237647, 6223681.297670785]},"properties": {"vejnavn": "Rosenkrantzgade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.557", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 14:37:23.03", "mi_style": null, "mi_prinx": 185}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574751.5470476393, 6223649.994042548]},"properties": {"vejnavn": "Park Alle 2", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.61", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.61", "mi_style": null, "mi_prinx": 186}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575067.4865718425, 6224480.395016832]},"properties": {"vejnavn": "Borggade 1", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.657", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.657", "mi_style": null, "mi_prinx": 187}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574636.8473534812, 6223433.838040478]},"properties": {"vejnavn": "Orla Lehmanns Alle", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.743", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.743", "mi_style": null, "mi_prinx": 188}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574714.8, 6223554.750000001]},"properties": {"vejnavn": "Johannes Bjergs Gade", "husnnr": "", "invalidepladser": 1, "bemrk": "På hjørner ved Banegårdspladsen 14", "ident": "1185", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.773", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:49:33.863", "mi_style": null, "mi_prinx": 189}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574779.9704729278, 6223780.417495232]},"properties": {"vejnavn": "Rådhuspladsen", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.81", "mi_style": null, "mi_prinx": 190}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575021.1449268986, 6223906.111910498]},"properties": {"vejnavn": "Ferdinand Sallings Stræde", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.847", "mi_style": null, "mi_prinx": 191}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575028.063190263, 6223924.808104941]},"properties": {"vejnavn": "Ferdinand Sallings Stræde", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.873", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.873", "mi_style": null, "mi_prinx": 192}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574539.4144572791, 6224578.814983732]},"properties": {"vejnavn": "Grønnegade", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.907", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.907", "mi_style": null, "mi_prinx": 193}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574533.8732499122, 6224584.343858345]},"properties": {"vejnavn": "Grønnegade", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.933", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.933", "mi_style": null, "mi_prinx": 194}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575460.8322248239, 6224695.803278689]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.97", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:10:54.057", "mi_style": null, "mi_prinx": 195}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575461.5348009364, 6224697.442622952]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.003", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:10:30.637", "mi_style": null, "mi_prinx": 196}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575455.3216124629, 6226039.447676172]},"properties": {"vejnavn": "Steen Billes Torv", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.037", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.037", "mi_style": null, "mi_prinx": 197}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573959.1296566332, 6223167.542244217]},"properties": {"vejnavn": "Ankersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.07", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.07", "mi_style": null, "mi_prinx": 198}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573965.1903521908, 6223169.331879942]},"properties": {"vejnavn": "Ankersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.093", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.093", "mi_style": null, "mi_prinx": 199}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573771.8681415829, 6222925.054833357]},"properties": {"vejnavn": "Læssøesgade", "husnnr": "85", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.127", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:14:58.7", "mi_style": null, "mi_prinx": 200}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573866.0093369979, 6222685.91027901]},"properties": {"vejnavn": "Harald Jenesens Plads", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.163", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.163", "mi_style": null, "mi_prinx": 201}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573940.9310782718, 6222759.405319308]},"properties": {"vejnavn": "Harald Jenesens Plads", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.19", "mi_style": null, "mi_prinx": 202}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574372.5907076919, 6222033.218436124]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.297", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:25.34", "mi_style": null, "mi_prinx": 203}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574368.4426301755, 6222031.638216118]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.323", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:16.533", "mi_style": null, "mi_prinx": 204}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574370.6154326841, 6222032.23079862]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.35", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:06.723", "mi_style": null, "mi_prinx": 205}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574136.7129774133, 6222538.070841889]},"properties": {"vejnavn": "Chr. Wærums Vej", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.387", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:47:59.483", "mi_style": null, "mi_prinx": 206}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574736.8941823252, 6222561.355631749]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.427", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.427", "mi_style": null, "mi_prinx": 207}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574086.8495389346, 6223044.81722454]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.5", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.5", "mi_style": null, "mi_prinx": 209}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575326.9832918391, 6224757.308651863]},"properties": {"vejnavn": "Nørreport", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.53", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.53", "mi_style": null, "mi_prinx": 210}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575469.2735810118, 6224466.787786544]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.567", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.567", "mi_style": null, "mi_prinx": 211}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575471.3432879301, 6224465.178114189]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.59", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.59", "mi_style": null, "mi_prinx": 212}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575473.4129948484, 6224463.568441832]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.617", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.617", "mi_style": null, "mi_prinx": 213}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575475.4744559224, 6224461.968767442]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.647", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.647", "mi_style": null, "mi_prinx": 214}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575477.5441628407, 6224460.129141892]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.673", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.673", "mi_style": null, "mi_prinx": 215}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575479.1521024784, 6224458.289516343]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.773", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.773", "mi_style": null, "mi_prinx": 216}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575481.2218093968, 6224456.219937599]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.797", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.797", "mi_style": null, "mi_prinx": 217}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575483.0523868304, 6224454.620263209]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.823", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.823", "mi_style": null, "mi_prinx": 218}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575485.1220937488, 6224452.320731272]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.85", "mi_style": null, "mi_prinx": 219}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575487.1918006671, 6224449.791246141]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.88", "mi_style": null, "mi_prinx": 220}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575515.433817381, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.907", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.907", "mi_style": null, "mi_prinx": 221}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575517.96529158, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.93", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.93", "mi_style": null, "mi_prinx": 222}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575520.9502872152, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.957", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.957", "mi_style": null, "mi_prinx": 223}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575523.9352828503, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.983", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.983", "mi_style": null, "mi_prinx": 224}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575526.9202784855, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.007", "mi_style": null, "mi_prinx": 225}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575529.6743904804, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.033", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.033", "mi_style": null, "mi_prinx": 226}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575532.6593861155, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.067", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.067", "mi_style": null, "mi_prinx": 227}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575535.4217439546, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.09", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.09", "mi_style": null, "mi_prinx": 228}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575538.4067395899, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.117", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.117", "mi_style": null, "mi_prinx": 229}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575540.9299679445, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.157", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.157", "mi_style": null, "mi_prinx": 230}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574394.576202219, 6223499.304714926]},"properties": {"vejnavn": "Valdemarsgade", "husnnr": "20", "invalidepladser": 1, "bemrk": null, "ident": "1102", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.19", "mi_style": null, "mi_prinx": 231}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575033.2828097024, 6222970.362379608]},"properties": {"vejnavn": "Heibergsgade", "husnnr": "27", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.223", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.223", "mi_style": null, "mi_prinx": 232}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573499.0, 6224197.1000000015]},"properties": {"vejnavn": "Regensburgsgade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.257", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:24:06.55", "mi_style": null, "mi_prinx": 233}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574459.3308174759, 6223990.544724442]},"properties": {"vejnavn": "Marstrandsgade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.287", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.287", "mi_style": null, "mi_prinx": 234}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574218.7335726058, 6224300.371660066]},"properties": {"vejnavn": "Mønsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.32", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.32", "mi_style": null, "mi_prinx": 235}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574841.6081590408, 6224887.112230719]},"properties": {"vejnavn": "Sjællandsgade", "husnnr": "92", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.353", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.353", "mi_style": null, "mi_prinx": 236}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224497.9]},"properties": {"vejnavn": "Tage Hansens Gade", "husnnr": "31B", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.38", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:18.623", "mi_style": null, "mi_prinx": 237}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573350.8007395364, 6225946.216653077]},"properties": {"vejnavn": "Hammershusvej", "husnnr": "41", "invalidepladser": 1, "bemrk": "Hammershusvej mellem nr. 41 og nr. 43", "ident": "1109", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.413", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.413", "mi_style": null, "mi_prinx": 238}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575124.3828974852, 6225601.696779101]},"properties": {"vejnavn": "Skt. Johannes Allé", "husnnr": "4", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": "1110", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.443", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:00:33.773", "mi_style": null, "mi_prinx": 239}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575252.1357631639, 6224570.226731846]},"properties": {"vejnavn": "Studsgade", "husnnr": "44", "invalidepladser": 2, "bemrk": null, "ident": "1111", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.473", "mi_style": null, "mi_prinx": 240}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574824.2671484861, 6226340.746347698]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "58", "invalidepladser": 1, "bemrk": null, "ident": "1111", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.507", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.507", "mi_style": null, "mi_prinx": 241}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575281.3650102671, 6223652.100041065]},"properties": {"vejnavn": "Toldbodgade", "husnnr": "6", "invalidepladser": 1, "bemrk": "Ud for svømmehallen", "ident": "1112", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.537", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:31:09.807", "mi_style": null, "mi_prinx": 242}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573871.8803781366, 6222609.255881792]},"properties": {"vejnavn": "Carl Bertelsens Gade", "husnnr": "3", "invalidepladser": 1, "bemrk": "I nærheden af Carl Bertelsens Gade nr. 3", "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.567", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.567", "mi_style": null, "mi_prinx": 243}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574261.0265080002, 6223960.050931365]},"properties": {"vejnavn": "Jerichausgade", "husnnr": "24", "invalidepladser": 1, "bemrk": "I den eksisterende skråparkering ud for nr. 24", "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.657", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.657", "mi_style": null, "mi_prinx": 245}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573354.775236487, 6224149.5423609335]},"properties": {"vejnavn": "Silkeborgvej", "husnnr": "108", "invalidepladser": 1, "bemrk": null, "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.717", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.717", "mi_style": null, "mi_prinx": 246}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574302.3464337677, 6219824.742661404]},"properties": {"vejnavn": "Kridthøjtorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.763", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.763", "mi_style": null, "mi_prinx": 247}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574881.526291278, 6222643.79885067]},"properties": {"vejnavn": "Assensgade", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1114", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.92", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.92", "mi_style": null, "mi_prinx": 248}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574120.6245171713, 6222766.663841857]},"properties": {"vejnavn": "Ingerslev Boulevard", "husnnr": "19", "invalidepladser": 1, "bemrk": "I nærheden af Ingerslev Boulevard nr. 19", "ident": "1114", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.0", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.0", "mi_style": null, "mi_prinx": 250}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574835.6134302375, 6222775.322079497]},"properties": {"vejnavn": "Hans Broges Gade", "husnnr": "17", "invalidepladser": 2, "bemrk": "Borger ønsker pladsen tidsbegrænset, da det er ved arbejdspladsen", "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.083", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.083", "mi_style": null, "mi_prinx": 252}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575090.5666900271, 6223833.916605643]},"properties": {"vejnavn": "Østergade", "husnnr": "35", "invalidepladser": 1, "bemrk": null, "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.143", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.143", "mi_style": null, "mi_prinx": 253}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575371.6015553251, 6224793.341317518]},"properties": {"vejnavn": "Helgenæsgade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.187", "mi_style": null, "mi_prinx": 254}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574443.9523178637, 6224528.715181402]},"properties": {"vejnavn": "Sejrøgade", "husnnr": "11", "invalidepladser": 1, "bemrk": "I nærheden af Sejrøgade nr.11", "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.243", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.243", "mi_style": null, "mi_prinx": 255}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575462.4460219346, 6224699.380442982]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 3, "bemrk": "Den ene af tre eksisterende HC-pladser ud for nr. 53-55 tidsbegrænset fra kl.8.00-14.00", "ident": "1118", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.29", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.29", "mi_style": null, "mi_prinx": 256}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572618.3471282512, 6224015.439657151]},"properties": {"vejnavn": "Silkeborgvej", "husnnr": "226", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.343", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.343", "mi_style": null, "mi_prinx": 257}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572934.9133366208, 6219773.593072755]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "137", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.393", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.393", "mi_style": null, "mi_prinx": 258}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572966.1156114371, 6219796.788351424]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "135", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.453", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.453", "mi_style": null, "mi_prinx": 259}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573958.7998228613, 6222500.128094437]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.513", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.513", "mi_style": null, "mi_prinx": 260}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574108.2557507273, 6222343.6199512165]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "2", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.57", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.57", "mi_style": null, "mi_prinx": 261}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574964.7928269794, 6222704.936406298]},"properties": {"vejnavn": "Strandparken", "husnnr": "36", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.613", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.613", "mi_style": null, "mi_prinx": 262}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574986.6525602082, 6222771.112936256]},"properties": {"vejnavn": "Strandparken", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.65", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.65", "mi_style": null, "mi_prinx": 263}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574174.5193554903, 6222819.103167984]},"properties": {"vejnavn": "Ingerslevs Boulevard", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.7", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.7", "mi_style": null, "mi_prinx": 264}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574259.3031265422, 6222874.021989416]},"properties": {"vejnavn": "Lundinsgade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.753", "mi_style": null, "mi_prinx": 265}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574732.0621175678, 6222787.729553992]},"properties": {"vejnavn": "Tietgens Plads", "husnnr": "11", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.81", "mi_style": null, "mi_prinx": 266}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574820.7791563489, 6223119.082108137]},"properties": {"vejnavn": "Brammersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.857", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.857", "mi_style": null, "mi_prinx": 267}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574401.956232864, 6223179.749759412]},"properties": {"vejnavn": "Godthåbsgade", "husnnr": null, "invalidepladser": 1, "bemrk": "Overfor nr. 2", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.963", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.963", "mi_style": null, "mi_prinx": 268}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574564.0530400358, 6223225.930359485]},"properties": {"vejnavn": "Dybbølsgade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.01", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.01", "mi_style": null, "mi_prinx": 269}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574140.167168153, 6223217.512073003]},"properties": {"vejnavn": "Absolonsgade", "husnnr": "28", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.043", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.043", "mi_style": null, "mi_prinx": 270}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574275.7206025358, 6223430.478724257]},"properties": {"vejnavn": "Sonnesgade", "husnnr": "7", "invalidepladser": 3, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.083", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.083", "mi_style": null, "mi_prinx": 271}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574256.1861973983, 6223461.63238302]},"properties": {"vejnavn": "Echersbergsgade", "husnnr": "43", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.15", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.15", "mi_style": null, "mi_prinx": 273}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574374.357392005, 6223467.071275949]},"properties": {"vejnavn": "Valdemarsgade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.187", "mi_style": null, "mi_prinx": 274}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574769.5764453339, 6223465.5386415245]},"properties": {"vejnavn": "Banegårdspladsen", "husnnr": "17", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.223", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:53:06.79", "mi_style": null, "mi_prinx": 275}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574924.5695985033, 6223516.741165786]},"properties": {"vejnavn": "Ny Banegårdsgade", "husnnr": "40", "invalidepladser": 2, "bemrk": "Overfor nr. 45", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.26", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.26", "mi_style": null, "mi_prinx": 276}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575241.6470492193, 6223772.769052051]},"properties": {"vejnavn": "Ridderstræde", "husnnr": null, "invalidepladser": 1, "bemrk": "Overfor nr 1", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.3", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.3", "mi_style": null, "mi_prinx": 277}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575132.5344444042, 6223962.394105587]},"properties": {"vejnavn": "Fiskergade", "husnnr": "84", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:17:48.05", "mi_style": null, "mi_prinx": 279}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574746.0553153381, 6224061.31032031]},"properties": {"vejnavn": "Christiansgade", "husnnr": "29S", "invalidepladser": 3, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.437", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.437", "mi_style": null, "mi_prinx": 281}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575181.3781732596, 6224118.438691995]},"properties": {"vejnavn": "Teatergaden", "husnnr": null, "invalidepladser": 3, "bemrk": "Sydlig endeaf vejen, på nordlig side", "ident": "E-8686", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.473", "mi_style": null, "mi_prinx": 282}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575222.3270360337, 6224141.414015435]},"properties": {"vejnavn": "Teatergaden", "husnnr": null, "invalidepladser": 4, "bemrk": "Nordlig ende af vejen , nordlig side", "ident": "E-8686", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.51", "mi_style": null, "mi_prinx": 283}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.7847335092, 6224144.713343865]},"properties": {"vejnavn": "Grønnegade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.587", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.587", "mi_style": null, "mi_prinx": 285}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574634.6704505871, 6224881.683335755]},"properties": {"vejnavn": "Falstergade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.66", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.66", "mi_style": null, "mi_prinx": 287}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573664.0273809661, 6224806.278684154]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "12", "invalidepladser": 2, "bemrk": "Overfor nr. 39", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.703", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.703", "mi_style": null, "mi_prinx": 288}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573900.8397833044, 6224443.072613699]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.74", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.74", "mi_style": null, "mi_prinx": 289}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573876.0610211948, 6224382.904860672]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.777", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.777", "mi_style": null, "mi_prinx": 290}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573858.2335058267, 6224344.252728201]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "27", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.81", "mi_style": null, "mi_prinx": 291}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574637.7049212881, 6224717.846684231]},"properties": {"vejnavn": "Hjarnøgade", "husnnr": "5", "invalidepladser": 2, "bemrk": "Overfor nr 5", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.843", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.843", "mi_style": null, "mi_prinx": 292}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574863.3524504495, 6224285.3847106155]},"properties": {"vejnavn": "Vestergade", "husnnr": "22", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.88", "mi_style": null, "mi_prinx": 293}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574537.1138667205, 6224140.554190449]},"properties": {"vejnavn": "Møllegade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.917", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.917", "mi_style": null, "mi_prinx": 294}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575074.9490609304, 6224661.438166022]},"properties": {"vejnavn": "Anholtsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.963", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.963", "mi_style": null, "mi_prinx": 295}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572960.45896225, 6225756.165337472]},"properties": {"vejnavn": "Musvågevej", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.073", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:29.073", "mi_style": null, "mi_prinx": 296}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575340.1766427128, 6224351.371279241]},"properties": {"vejnavn": "Olufgade", "husnnr": "3", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.113", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:00:13.91", "mi_style": null, "mi_prinx": 297}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573871.0393020185, 6222888.768987707]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:09:34.883", "mi_style": null, "mi_prinx": 298}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574914.8559939226, 6222594.038979148]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "34", "invalidepladser": 1, "bemrk": "5x3,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:19.733", "mi_style": null, "mi_prinx": 299}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575638.25, 6226153.625]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "222", "invalidepladser": 1, "bemrk": "5x3,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.26", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:56:12.1", "mi_style": null, "mi_prinx": 301}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574462.8600388346, 6226572.059264624]},"properties": {"vejnavn": "Stockholmsgade", "husnnr": "1", "invalidepladser": 1, "bemrk": "5x2,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.293", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:19.97", "mi_style": null, "mi_prinx": 302}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574497.212226172, 6224409.829380264]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "3", "invalidepladser": 1, "bemrk": "Til højre for porten", "ident": "1124", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.38", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.12", "mi_style": null, "mi_prinx": 304}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575140.6024732155, 6224850.239736008]},"properties": {"vejnavn": "Høegh Guldbergs Gade", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": "1130", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.423", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.197", "mi_style": null, "mi_prinx": 305}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574098.9791758942, 6222367.355119992]},"properties": {"vejnavn": "P. P. Ørumsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1127", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.467", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.267", "mi_style": null, "mi_prinx": 306}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573984.3949235562, 6222175.364199189]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "1129", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.507", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.337", "mi_style": null, "mi_prinx": 307}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574502.2421911926, 6222857.645322839]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.543", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:03:19.153", "mi_style": null, "mi_prinx": 308}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574505.5487747552, 6222858.265196665]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.57", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:03:07.203", "mi_style": null, "mi_prinx": 309}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574509.0697502696, 6222858.8850704925]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.6", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:02:53.353", "mi_style": null, "mi_prinx": 310}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.586833246, 6222612.475226503]},"properties": {"vejnavn": "Assensgade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": "1121", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.633", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.67", "mi_style": null, "mi_prinx": 311}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574719.5036967049, 6222608.076121928]},"properties": {"vejnavn": "Assensgade", "husnnr": "21", "invalidepladser": 2, "bemrk": null, "ident": "1128", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.677", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.737", "mi_style": null, "mi_prinx": 312}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576416.2510775031, 6232254.552609537]},"properties": {"vejnavn": "Lystruplund", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for børnehavens indgang", "ident": "1123", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.717", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.81", "mi_style": null, "mi_prinx": 313}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573121.6899557706, 6220138.458805391]},"properties": {"vejnavn": "Rundhøj Alle", "husnnr": "61", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.883", "mi_style": null, "mi_prinx": 314}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576412.5899226356, 6232262.860918405]},"properties": {"vejnavn": "Lystruplund", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for børnehavens indgang", "ident": "1123", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.783", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.953", "mi_style": null, "mi_prinx": 315}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575079.1956707429, 6224575.955565759]},"properties": {"vejnavn": "Borggade", "husnnr": "16", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.86", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.13", "mi_style": null, "mi_prinx": 317}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574136.9018138117, 6222534.351128438]},"properties": {"vejnavn": "Chr. Wærumsgade", "husnnr": "29", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.943", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.277", "mi_style": null, "mi_prinx": 319}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573894.8368086569, 6224475.935924468]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "6", "invalidepladser": 2, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.347", "mi_style": null, "mi_prinx": 320}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576295.1526081712, 6228305.306467168]},"properties": {"vejnavn": "Fortebakken", "husnnr": "34A", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.047", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.417", "mi_style": null, "mi_prinx": 321}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575393.7251555714, 6223907.781570643]},"properties": {"vejnavn": "Hack Kampmanns plads", "husnnr": "2", "invalidepladser": 3, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.09", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.487", "mi_style": null, "mi_prinx": 322}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574911.6071312699, 6222880.060760242]},"properties": {"vejnavn": "Hendrik Pontopidans gade", "husnnr": "2", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.123", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.56", "mi_style": null, "mi_prinx": 323}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575107.1408370622, 6226017.862069859]},"properties": {"vejnavn": "Herambsgade", "husnnr": "5", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.16", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.6", "mi_style": null, "mi_prinx": 324}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575153.6061696704, 6211381.751207897]},"properties": {"vejnavn": "Mallingsgårdsvej", "husnnr": "137", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.65", "mi_style": null, "mi_prinx": 325}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573888.9575216738, 6222869.90282786]},"properties": {"vejnavn": "Max Mullers Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.227", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.71", "mi_style": null, "mi_prinx": 326}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574546.3132659666, 6223169.7029371355]},"properties": {"vejnavn": "Montaangade", "husnnr": "8", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.263", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:58:27.443", "mi_style": null, "mi_prinx": 327}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573574.4692660668, 6222862.3343683975]},"properties": {"vejnavn": "Nordborg Gade", "husnnr": "41", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.3", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.947", "mi_style": null, "mi_prinx": 328}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574323.8351040032, 6223091.617698438]},"properties": {"vejnavn": "Ole Rømers Gade", "husnnr": "38", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.333", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.02", "mi_style": null, "mi_prinx": 329}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574564.1684818559, 6223099.706052078]},"properties": {"vejnavn": "Ole Rømers Gade", "husnnr": "63", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.373", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.09", "mi_style": null, "mi_prinx": 330}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573623.1032557249, 6222316.575456046]},"properties": {"vejnavn": "Rudolp Wulffs Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.17", "mi_style": null, "mi_prinx": 331}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572704.5079553006, 6226851.832317628]},"properties": {"vejnavn": "Sandkåsvej", "husnnr": "27", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.22", "mi_style": null, "mi_prinx": 332}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574883.422835466, 6227413.9079087665]},"properties": {"vejnavn": "Solhvervsvej", "husnnr": "22", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.547", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.29", "mi_style": null, "mi_prinx": 333}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574839.5219604338, 6224676.74505035]},"properties": {"vejnavn": "Thunøgade", "husnnr": "21", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.59", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:26:13.553", "mi_style": null, "mi_prinx": 334}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574010.542495819, 6222959.064679222]},"properties": {"vejnavn": "Trepkasgade", "husnnr": "6", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.64", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.437", "mi_style": null, "mi_prinx": 335}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573839.0454261499, 6225405.03680867]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "78", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.68", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.533", "mi_style": null, "mi_prinx": 336}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574665.6000000001, 6227253.1000000015]},"properties": {"vejnavn": "Vorregårds Allé", "husnnr": "95", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.717", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:50:45.603", "mi_style": null, "mi_prinx": 337}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574079.8900463488, 6222655.326504249]},"properties": {"vejnavn": "Ålborgade", "husnnr": "30", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.66", "mi_style": null, "mi_prinx": 338}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575257.8748707939, 6225740.82845925]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "86", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.79", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.73", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 339}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575579.6936819801, 6225552.636765125]},"properties": {"vejnavn": "Trøjborgvej", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for Kochs Skole", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.86", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.893", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 341}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575354.5079200993, 6225296.238954158]},"properties": {"vejnavn": "Kirkegårdsvej", "husnnr": "53", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.9", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.95", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 342}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575673.3747190277, 6225441.769331871]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44B", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.953", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.047", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 343}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575589.1104361663, 6225071.994598448]},"properties": {"vejnavn": "Skovvejen", "husnnr": "19B", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.12", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 344}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575246.1905094264, 6225228.322778299]},"properties": {"vejnavn": "Peter Sabroes Gade", "husnnr": "2-4", "invalidepladser": 10, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.04", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.21", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 345}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575188.106782205, 6225234.641492143]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 3, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.08", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.3", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 346}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575106.3244984768, 6225400.537724445]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 4, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.117", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.37", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 347}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575064.3119217885, 6225293.259560605]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.16", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.437", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 348}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575109.6475737281, 6225313.485443685]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.193", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.503", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 349}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575068.0720267874, 6225440.379614743]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.223", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.567", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 350}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.441055486, 6225637.589473247]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 5, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.273", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.637", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 351}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574422.6368103585, 6225613.944286156]},"properties": {"vejnavn": "Victor Albecks Vej", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.303", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.71", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 352}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574900.5989291347, 6225286.400956654]},"properties": {"vejnavn": "Vennelyst Boulevard", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.34", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.78", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 353}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574878.0960200508, 6225366.764598867]},"properties": {"vejnavn": "Bartholins Allé", "husnnr": null, "invalidepladser": 1, "bemrk": "V/Bartholinbygningen", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.373", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.847", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 354}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574170.8004797128, 6225423.143123182]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.41", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.913", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 355}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575117.8851721799, 6224865.706587776]},"properties": {"vejnavn": "Høegh-Guldbergs Gade", "husnnr": "49", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.033", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 357}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574990.5281070274, 6224945.140419249]},"properties": {"vejnavn": "Høegh-Guldbergs Gade", "husnnr": "6A", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.103", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 358}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574350.6093604325, 6224544.851896821]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "41", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.597", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.153", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 359}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574519.1874012209, 6224508.419312567]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.63", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.28", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 360}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574525.4130136644, 6224532.194473202]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.667", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.357", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 361}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573850.8204918046, 6224325.876468635]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.757", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.57", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 364}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573842.8467603703, 6224172.757635531]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.787", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.62", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 365}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573685.0130546996, 6224142.763740702]},"properties": {"vejnavn": "Regenburgs Plads", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.817", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.683", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 366}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573708.2663356145, 6223856.911925012]},"properties": {"vejnavn": "Åparken", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.737", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 367}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573789.7847523252, 6223920.838912859]},"properties": {"vejnavn": "Åparken", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.803", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 368}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574850.3899832161, 6222925.211569925]},"properties": {"vejnavn": "Odensegade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.923", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.9", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 369}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574954.056737706, 6223018.042674422]},"properties": {"vejnavn": "Marselisborg Allé", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.953", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.97", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 370}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574731.5426293771, 6223104.335109847]},"properties": {"vejnavn": "Sankt Pauls Gade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.987", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.04", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 371}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574744.3401797245, 6223104.785018269]},"properties": {"vejnavn": "Sankt Pauls Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.027", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.113", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 372}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574725.6138673283, 6222733.070679648]},"properties": {"vejnavn": "J. Michaelsens Gade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.123", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.31", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 375}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574543.1745622781, 6223191.127443518]},"properties": {"vejnavn": "Montanagade", "husnnr": "4A", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.153", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.35", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 376}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574802.1600399285, 6223111.60363036]},"properties": {"vejnavn": "Brammersgade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.393", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 377}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574870.3036971911, 6223125.000903384]},"properties": {"vejnavn": "Brammersgade", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.217", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.43", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 378}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.7603455347, 6223146.6087693]},"properties": {"vejnavn": "St. St. Blichers Gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:03:17.84", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 379}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574131.5007857978, 6222588.660074009]},"properties": {"vejnavn": "Chr. Wærums Gade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.293", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.507", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 380}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574200.5290566034, 6222565.141509435]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.327", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-24 13:56:15.127", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 381}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574191.027535771, 6222648.197955245]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.36", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.58", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 382}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574193.9630563403, 6222718.033740373]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.39", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.617", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 383}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575232.5353912722, 6224544.841898856]},"properties": {"vejnavn": "Studsgade", "husnnr": "38", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.423", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.673", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 384}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574716.4527343153, 6224380.4153674]},"properties": {"vejnavn": "Klostergade", "husnnr": "78", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.457", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.71", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 385}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574748.306430831, 6224145.5731688505]},"properties": {"vejnavn": "Grønnegade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.497", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.747", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 386}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574797.3197293266, 6224474.496217515]},"properties": {"vejnavn": "Gammel Munkegade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.863", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.827", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 388}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574841.4597338435, 6224276.456527921]},"properties": {"vejnavn": "Vestergade", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.897", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.863", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 389}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.8203643236, 6224302.431240844]},"properties": {"vejnavn": "Badstuegade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.927", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.9", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 390}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574934.1924987969, 6223330.958981213]},"properties": {"vejnavn": "M.P. Bruuns Gade", "husnnr": "25", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.957", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.933", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 391}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574915.3095153589, 6223254.954451715]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "124", "invalidepladser": 1, "bemrk": "8-16", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.997", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.97", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 392}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574915.6393491307, 6223263.812648655]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "97", "invalidepladser": 1, "bemrk": "8-16", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.043", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.007", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 393}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574488.7107607027, 6223237.937915382]},"properties": {"vejnavn": "Istedgade", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.077", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.043", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 394}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574767.2962480201, 6223464.91676953]},"properties": {"vejnavn": "Banegårdspladsen", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.19", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:52:54.773", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 396}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574530.2173944163, 6223395.66940094]},"properties": {"vejnavn": "Kriegersvej", "husnnr": "11", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.22", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:04:44.057", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 397}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574906.5854120937, 6223675.0989325205]},"properties": {"vejnavn": "Rosenkrantzgade", "husnnr": "24", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 14:37:07.49", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 398}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575020.6317082599, 6224083.022794313]},"properties": {"vejnavn": "Fiskergade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.287", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:55:01.553", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 399}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574872.5218293066, 6223967.619390828]},"properties": {"vejnavn": "Telefonsmøgen", "husnnr": null, "invalidepladser": 3, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.327", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.6", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 400}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574997.9, 6222462.25]},"properties": {"vejnavn": "Adolph Meyers Vej", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud mod Strandvejen", "ident": "1196", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-16 11:55:35.953", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-16 12:00:59.113", "mi_style": null, "mi_prinx": 404}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573880.2000000001, 6222878.7]},"properties": {"vejnavn": "Max Müllers Gade 11", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": "1169", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:08:44.613", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:08:44.613", "mi_style": null, "mi_prinx": 405}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573875.6000000001, 6222883.899999999]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "1169", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:11:20.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:11:20.153", "mi_style": null, "mi_prinx": 406}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574233.0, 6226255.500000001]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "51", "invalidepladser": 1, "bemrk": null, "ident": "1171", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:14:12.3", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:14:12.3", "mi_style": null, "mi_prinx": 407}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573849.4, 6222996.65]},"properties": {"vejnavn": "Læssøegade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": "1173", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:18:31.19", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:18:31.19", "mi_style": null, "mi_prinx": 408}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574396.7, 6224597.35]},"properties": {"vejnavn": "Skt. Marcus Kirkeplads", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1173", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:21:54.81", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:21:54.81", "mi_style": null, "mi_prinx": 409}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574845.2999999999, 6224678.449999999]},"properties": {"vejnavn": "Thunøgade 26B", "husnnr": "26B", "invalidepladser": 1, "bemrk": null, "ident": "1176", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:25:55.553", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:25:55.553", "mi_style": null, "mi_prinx": 410}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573925.4, 6222166.899999999]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": "1181", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:31:40.443", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:31:40.443", "mi_style": null, "mi_prinx": 412}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571908.4, 6223314.500000001]},"properties": {"vejnavn": "Henrik Hertz Vej", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": "1182", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:34:51.437", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:34:51.437", "mi_style": null, "mi_prinx": 413}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574719.1, 6223555.250000001]},"properties": {"vejnavn": "Johannes Bjergs Gade", "husnnr": "", "invalidepladser": 1, "bemrk": "På hjørnet ved Banegårdspladsen 14", "ident": "1185", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:48:20.697", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:48:20.697", "mi_style": null, "mi_prinx": 416}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575397.9500000001, 6225939.025]},"properties": {"vejnavn": "Ivar Huitfeldts Gade", "husnnr": "64A", "invalidepladser": 1, "bemrk": null, "ident": "1185", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:51:46.78", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:51:46.78", "mi_style": null, "mi_prinx": 417}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574223.2, 6222506.7]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:54:59.317", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:54:59.317", "mi_style": null, "mi_prinx": 418}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576141.0, 6225269.5]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:59:53.823", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:59:53.823", "mi_style": null, "mi_prinx": 419}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576136.6, 6225275.9]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:00:00.397", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:00:00.397", "mi_style": null, "mi_prinx": 420}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576132.525, 6225282.1125]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:00:19.683", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:01:17.037", "mi_style": null, "mi_prinx": 421}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574134.4, 6222663.3500000015]},"properties": {"vejnavn": "Ålborggade", "husnnr": "16", "invalidepladser": 1, "bemrk": "på NV hjørnet ved Chr. Wærums Gade", "ident": "1187", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:08:44.067", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:08:44.067", "mi_style": null, "mi_prinx": 422}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573822.0, 6222563.450000001]},"properties": {"vejnavn": "P. P. Ørums Gade", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": "1187", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:12:21.367", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:12:21.367", "mi_style": null, "mi_prinx": 423}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570801.2, 6216735.900000001]},"properties": {"vejnavn": "Kirketorvet", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": "1190", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:14:51.303", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:14:51.303", "mi_style": null, "mi_prinx": 424}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570806.0, 6216734.500000001]},"properties": {"vejnavn": "Kirketorvet", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": "1190", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:15:00.997", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:15:00.997", "mi_style": null, "mi_prinx": 425}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574101.2, 6223033.899999999]},"properties": {"vejnavn": "Schleppegrellsgade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:19:45.767", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:19:45.767", "mi_style": null, "mi_prinx": 426}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574902.6, 6222755.500000001]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:22:33.383", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:22:33.383", "mi_style": null, "mi_prinx": 427}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573502.0, 6224196.900000001]},"properties": {"vejnavn": "Regensburgsgade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:24:22.523", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:25:10.057", "mi_style": null, "mi_prinx": 428}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574308.35, 6223515.425000001]},"properties": {"vejnavn": "Christen Købkes Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:27:59.887", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:27:59.887", "mi_style": null, "mi_prinx": 430}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575200.2, 6224522.150000001]},"properties": {"vejnavn": "Studsgade", "husnnr": "22", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:29:06.1", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:29:06.1", "mi_style": null, "mi_prinx": 431}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574790.4, 6222623.850000001]},"properties": {"vejnavn": "Assensgade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:31:34.91", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:31:34.91", "mi_style": null, "mi_prinx": 432}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575263.2000000001, 6228819.3]},"properties": {"vejnavn": "Tværmarksvej", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "1200", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:36:06.61", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:36:06.61", "mi_style": null, "mi_prinx": 433}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574008.1, 6225215.550000001]},"properties": {"vejnavn": "Chr. Molbechs Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1202", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:42:59.763", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:42:59.763", "mi_style": null, "mi_prinx": 434}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575616.4500000001, 6226158.575000001]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "218", "invalidepladser": 1, "bemrk": "", "ident": "1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:22:30.467", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:22:30.467", "mi_style": null, "mi_prinx": 435}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574331.2000000001, 6227559.55]},"properties": {"vejnavn": "Vorregårds Allé", "husnnr": "125", "invalidepladser": 1, "bemrk": null, "ident": "1205", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:27:08.61", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:27:08.61", "mi_style": null, "mi_prinx": 436}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574076.6, 6222877.450000001]},"properties": {"vejnavn": "Vejlegade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1207", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:35:54.443", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:35:54.443", "mi_style": null, "mi_prinx": 437}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574813.4, 6227367.300000002]},"properties": {"vejnavn": "Solhvervsvej", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:36:25.903", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:58:57.687", "mi_style": null, "mi_prinx": 438}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573701.05, 6225537.125000001]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "37", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:39:11.127", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:39:11.127", "mi_style": null, "mi_prinx": 439}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570415.7999999999, 6224631.05]},"properties": {"vejnavn": "Karen Blixens Boulevard", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:42:06.563", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:42:06.563", "mi_style": null, "mi_prinx": 440}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573264.6000000001, 6225891.1000000015]},"properties": {"vejnavn": "Majdalen", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1209", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:46:08.143", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 07:00:59.987", "mi_style": null, "mi_prinx": 441}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575311.4, 6225676.650000001]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "1213", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:52:54.42", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:52:54.42", "mi_style": null, "mi_prinx": 442}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575013.2, 6224297.75]},"properties": {"vejnavn": "Badstuegade", "husnnr": "1B", "invalidepladser": 1, "bemrk": null, "ident": "1213", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:06:29.84", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:06:29.84", "mi_style": null, "mi_prinx": 443}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574438.2, 6224426.775000001]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": "1216", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:12:55.713", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:12:55.713", "mi_style": null, "mi_prinx": 444}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574116.1, 6222681.774999999]},"properties": {"vejnavn": "Chr. Wærums Gade", "husnnr": "11A", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:15:24.13", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:15:24.13", "mi_style": null, "mi_prinx": 445}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575674.7999999999, 6225450.7]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44B", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:25:59.593", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:26:38.423", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 446}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575677.0, 6225459.9]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44A", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:27:24.777", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:27:24.777", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 447}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575345.5377970586, 6224349.986968176]},"properties": {"vejnavn": "Skt. Olufs Gade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:32:05.217", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:59:53.663", "mi_style": null, "mi_prinx": 448}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575681.3, 6226046.85]},"properties": {"vejnavn": "Peder Skrams Gade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "1195+1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:44:10.597", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:44:10.597", "mi_style": null, "mi_prinx": 449}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575668.2000000001, 6226049.55]},"properties": {"vejnavn": "Peder Skrams Gade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": "1195+1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:44:22.327", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:44:22.327", "mi_style": null, "mi_prinx": 450}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224469.1]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "29", "invalidepladser": null, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:47:52.29", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:58.087", "mi_style": null, "mi_prinx": 451}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.6000000001, 6224461.9]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:48:07.09", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:44.383", "mi_style": null, "mi_prinx": 452}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224492.5]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "31B", "invalidepladser": 1, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:48:39.0", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:48:39.0", "mi_style": null, "mi_prinx": 453}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574462.6000000001, 6222419.500000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:06.373", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:06.373", "mi_style": null, "mi_prinx": 454}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574468.6000000001, 6222420.700000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:12.95", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:12.95", "mi_style": null, "mi_prinx": 455}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574475.0000000001, 6222421.900000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:20.613", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:20.613", "mi_style": null, "mi_prinx": 456}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574481.8, 6222423.1000000015]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:32.217", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:32.217", "mi_style": null, "mi_prinx": 457}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576152.8, 6225252.3]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:51:19.7", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:51:19.7", "mi_style": null, "mi_prinx": 458}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576138.6000000001, 6225293.1]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:51:52.03", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:51:52.03", "mi_style": null, "mi_prinx": 459}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576141.8, 6225288.5]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "10", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:52:02.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:52:02.153", "mi_style": null, "mi_prinx": 460}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576156.4, 6225267.7]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:52:20.473", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:52:20.473", "mi_style": null, "mi_prinx": 461}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574173.0585387659, 6222819.027722998]},"properties": {"vejnavn": "Ingerslev Boulevard", "husnnr": null, "invalidepladser": 1, "bemrk": "1200", "ident": "1", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 13:11:36.553", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 13:11:36.553", "mi_style": null, "mi_prinx": 464}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574717.2467898262, 6222608.072481987]},"properties": {"vejnavn": "Assensgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:16:10.33", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:16:10.33", "mi_style": null, "mi_prinx": 466}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574756.4414590374, 6222616.351223999]},"properties": {"vejnavn": "Assensgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:17:39.227", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:17:39.227", "mi_style": null, "mi_prinx": 467}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575071.0398722045, 6223736.640195375]},"properties": {"vejnavn": "Fredensgade", "husnnr": "36", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:23:36.633", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:23:36.633", "mi_style": null, "mi_prinx": 468}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575071.6634185021, 6223738.822607417]},"properties": {"vejnavn": "Fredensgade ", "husnnr": "36", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:24:07.44", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:24:07.44", "mi_style": null, "mi_prinx": 469}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574458.5545235131, 6224405.154470327]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:01:44.29", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:01:44.29", "mi_style": null, "mi_prinx": 470}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574456.8303596153, 6224406.480750249]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:02:10.15", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:02:10.15", "mi_style": null, "mi_prinx": 471}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574439.588720638, 6224425.8444371]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:02:36.877", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:02:36.877", "mi_style": null, "mi_prinx": 472}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574213.7462111231, 6224317.430271916]},"properties": {"vejnavn": "Mønsgade", "husnnr": "8a", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:06:38.743", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:06:38.743", "mi_style": null, "mi_prinx": 473}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574364.2445136809, 6225576.13351622]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:14:03.827", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:14:03.827", "mi_style": null, "mi_prinx": 474}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574271.226033188, 6225520.486698972]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:14:25.237", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:14:54.583", "mi_style": null, "mi_prinx": 475}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573648.4455852153, 6222703.284394252]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "9", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:18:50.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:10.813", "mi_style": null, "mi_prinx": 476}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573645.981519507, 6222704.927104725]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "9", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:19:02.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:24.89", "mi_style": null, "mi_prinx": 477}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574615.0200821363, 6226143.664271046]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:24:58.847", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:25:25.463", "mi_style": null, "mi_prinx": 478}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573440.6894959002, 6225916.164055831]},"properties": {"vejnavn": "Fuglesangs Allé ", "husnnr": "115", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:30:49.483", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:31:19.2", "mi_style": null, "mi_prinx": 479}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573441.7161899453, 6225914.829353572]},"properties": {"vejnavn": "Fuglesangs Allé", "husnnr": "115", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:31:13.193", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:31:13.193", "mi_style": null, "mi_prinx": 480}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572528.9192842386, 6223635.9144780645]},"properties": {"vejnavn": "Åbyvej ", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:33:23.21", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:33:23.21", "mi_style": null, "mi_prinx": 481}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [577384.3500328106, 6228392.545887083]},"properties": {"vejnavn": "Vestre Strandallé", "husnnr": "170", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:35:52.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:35:52.45", "mi_style": null, "mi_prinx": 482}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [577384.3500328106, 6228395.831308028]},"properties": {"vejnavn": "Vestre Strandallé", "husnnr": "170", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:36:07.74", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:36:07.74", "mi_style": null, "mi_prinx": 483}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574290.6883509178, 6223440.857930892]},"properties": {"vejnavn": "Christen Købkes Gade", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:04:38.56", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:04:38.56", "mi_style": null, "mi_prinx": 485}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574292.8885474477, 6223450.555093376]},"properties": {"vejnavn": "Christen Købkes gade", "husnnr": "25", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": "", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:05:17.92", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:05:17.92", "mi_style": null, "mi_prinx": 486}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574188.2994045179, 6222220.596509239]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:09:47.597", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:09:47.597", "mi_style": null, "mi_prinx": 487}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574594.8774293908, 6224862.872887781]},"properties": {"vejnavn": "Falstergade ", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:11:45.25", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:11:45.25", "mi_style": null, "mi_prinx": 488}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575645.3796509246, 6225033.394188903]},"properties": {"vejnavn": "Fiskerivej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:13:23.887", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:13:23.887", "mi_style": null, "mi_prinx": 489}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575687.3336841018, 6225135.283751449]},"properties": {"vejnavn": "Fiskerivej", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:21:28.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:21:28.367", "mi_style": null, "mi_prinx": 490}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573765.5234907598, 6222669.554414783]},"properties": {"vejnavn": "Gråstengade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:23:07.33", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:23:07.33", "mi_style": null, "mi_prinx": 491}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574263.9406273357, 6223167.701885861]},"properties": {"vejnavn": "Holbergsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:37:05.283", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:37:05.283", "mi_style": null, "mi_prinx": 492}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573818.8194244037, 6222564.275600996]},"properties": {"vejnavn": "P.P. Ørums Gade ", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:54:37.353", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:54:37.353", "mi_style": null, "mi_prinx": 494}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575020.143846024, 6224751.283552042]},"properties": {"vejnavn": "Sjællandsgade ", "husnnr": "44", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:55:58.737", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:55:58.737", "mi_style": null, "mi_prinx": 495}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575126.0758521565, 6225602.137577002]},"properties": {"vejnavn": "Skt. Johannes Allé", "husnnr": "4", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": "", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:58:18.097", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:58:18.097", "mi_style": null, "mi_prinx": 497}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575127.7185626287, 6225602.958932239]},"properties": {"vejnavn": "Skt. Johannes Allé ", "husnnr": "4", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:58:54.79", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:58:54.79", "mi_style": null, "mi_prinx": 498}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574192.3717864477, 6222728.518480494]},"properties": {"vejnavn": "Stadion Allé ", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:01:35.56", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:01:35.56", "mi_style": null, "mi_prinx": 499}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574516.2188090354, 6224495.0]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:04:13.31", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:04:13.31", "mi_style": null, "mi_prinx": 500}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573382.1503490765, 6222436.561601641]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:05:22.27", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:05:22.27", "mi_style": null, "mi_prinx": 501}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574586.7241332495, 6222319.2006106265]},"properties": {"vejnavn": "Marselis tværvej", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:10:21.543", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:10:21.543", "mi_style": null, "mi_prinx": 502}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575512.1720674813, 6225984.295569786]},"properties": {"vejnavn": "Herluf Trolles Gade", "husnnr": "43", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:11:37.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:11:37.07", "mi_style": null, "mi_prinx": 503}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570549.4435318275, 6223329.651950718]},"properties": {"vejnavn": "Louisevej", "husnnr": "60", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:12:39.197", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:12:39.197", "mi_style": null, "mi_prinx": 504}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575432.3019691993, 6213534.279897331]},"properties": {"vejnavn": "Skoleparken", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:15:20.913", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:15:20.913", "mi_style": null, "mi_prinx": 505}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575435.1767125258, 6213534.895913758]},"properties": {"vejnavn": "Skoleparken ", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:15:35.137", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:15:35.137", "mi_style": null, "mi_prinx": 506}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574686.8167566396, 6224112.425570103]},"properties": {"vejnavn": "Blegdammen", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:46:05.277", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:46:05.277", "mi_style": null, "mi_prinx": 507}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.8197535932, 6222559.590349074]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:48:59.607", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:48:59.607", "mi_style": null, "mi_prinx": 508}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574724.2673921969, 6222578.276180697]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:49:32.92", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:49:32.92", "mi_style": null, "mi_prinx": 509}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574662.6690560804, 6222698.830589775]},"properties": {"vejnavn": "Fåborggade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:52:16.67", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:52:16.67", "mi_style": null, "mi_prinx": 510}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573723.7641226394, 6222883.87392884]},"properties": {"vejnavn": "Haderslevgade", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:56:02.037", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:56:02.037", "mi_style": null, "mi_prinx": 511}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574950.1948158465, 6222518.823101973]},"properties": {"vejnavn": "Kildegården ", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:02:38.327", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:02:38.327", "mi_style": null, "mi_prinx": 512}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573174.3326094088, 6222324.340775997]},"properties": {"vejnavn": "Kirkedammen", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:03:44.91", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:03:44.91", "mi_style": null, "mi_prinx": 513}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574764.3736858368, 6224401.6597698275]},"properties": {"vejnavn": "Klostergade", "husnnr": "68", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:04:40.63", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:04:40.63", "mi_style": null, "mi_prinx": 514}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575484.2643121149, 6224755.312114988]},"properties": {"vejnavn": "Kystvejen", "husnnr": "63", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:05:45.95", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:05:45.95", "mi_style": null, "mi_prinx": 516}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575260.0545790552, 6225740.691868583]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "64", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:06:42.113", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:06:42.113", "mi_style": null, "mi_prinx": 517}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574122.1695396319, 6222253.575999677]},"properties": {"vejnavn": "Ordrupvej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:07:29.987", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:07:29.987", "mi_style": null, "mi_prinx": 518}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573820.2902647848, 6222564.066232556]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:08:36.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:08:36.257", "mi_style": null, "mi_prinx": 519}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573513.2284804934, 6224421.503059546]},"properties": {"vejnavn": "Thomas Nielsens Gade ", "husnnr": "4b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:35:15.567", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:35:15.567", "mi_style": null, "mi_prinx": 521}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574869.9698357292, 6224686.401437372]},"properties": {"vejnavn": "Thunøgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:36:20.32", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:36:20.32", "mi_style": null, "mi_prinx": 522}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574842.0437577005, 6224677.982546201]},"properties": {"vejnavn": "Thunøgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:36:47.17", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:36:47.17", "mi_style": null, "mi_prinx": 523}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573819.4550102665, 6222787.195071867]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:38:32.23", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:38:32.23", "mi_style": null, "mi_prinx": 524}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573819.8656878845, 6222774.258726898]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "3", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:38:44.137", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:38:44.137", "mi_style": null, "mi_prinx": 525}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573809.5987474328, 6222763.17043121]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "5", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:39:04.733", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:39:04.733", "mi_style": null, "mi_prinx": 526}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574862.9051540041, 6226224.204312115]},"properties": {"vejnavn": "Finsensgade", "husnnr": "61", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:40:40.31", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:40:40.31", "mi_style": null, "mi_prinx": 527}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575609.3253388093, 6225642.107802872]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "14", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:41:40.133", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:41:40.133", "mi_style": null, "mi_prinx": 528}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575621.7098357282, 6226157.82032854]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "220", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:42:29.26", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:42:29.26", "mi_style": null, "mi_prinx": 529}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574273.5949691997, 6226579.6673511285]},"properties": {"vejnavn": "Frederikshaldsgade ", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:46:09.1", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:46:09.1", "mi_style": null, "mi_prinx": 530}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574272.9789527727, 6226577.408624229]},"properties": {"vejnavn": "Frederikshaldsgade", "husnnr": "15", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:46:27.437", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:46:27.437", "mi_style": null, "mi_prinx": 531}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574550.2745468095, 6226415.597575942]},"properties": {"vejnavn": "Norges Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:48:32.27", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:48:32.27", "mi_style": null, "mi_prinx": 532}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573464.9690871622, 6225551.886526177]},"properties": {"vejnavn": "Fuglesangs Allé", "husnnr": "114", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:49:51.593", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:49:51.593", "mi_style": null, "mi_prinx": 533}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573284.2962628339, 6226064.2587268995]},"properties": {"vejnavn": "Hammershusvej ", "husnnr": "51", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:50:32.377", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:50:32.377", "mi_style": null, "mi_prinx": 534}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574168.5019342923, 6225689.519246406]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "92d", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:52:34.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:52:34.45", "mi_style": null, "mi_prinx": 536}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573183.6454293831, 6219583.845422071]},"properties": {"vejnavn": "M.P. Hansens Vej", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:54:27.033", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:54:27.033", "mi_style": null, "mi_prinx": 537}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573194.0588431241, 6219583.965116482]},"properties": {"vejnavn": "M.P. Hansens Gade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:54:45.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:54:45.367", "mi_style": null, "mi_prinx": 538}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574293.6617659132, 6222509.108829569]},"properties": {"vejnavn": "F. Vestergaards Gade", "husnnr": "32", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:56:36.283", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:56:36.283", "mi_style": null, "mi_prinx": 539}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574290.7870225868, 6222525.535934292]},"properties": {"vejnavn": "F. Vestergaards Gade ", "husnnr": "32", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:56:51.647", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:56:51.647", "mi_style": null, "mi_prinx": 540}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.7018685832, 6224472.544147842]},"properties": {"vejnavn": "Guldsmedegade", "husnnr": "26", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:58:17.377", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:58:17.377", "mi_style": null, "mi_prinx": 541}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574853.3419507187, 6224400.072895276]},"properties": {"vejnavn": "Klostergade ", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:01:19.687", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:46.85", "mi_style": null, "mi_prinx": 542}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574850.2618685832, 6224400.278234085]},"properties": {"vejnavn": "Klostergade", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:01:29.593", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:29.593", "mi_style": null, "mi_prinx": 543}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573800.5521852324, 6222947.219776177]},"properties": {"vejnavn": "Læssøesgade", "husnnr": "83", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:15:14.473", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:15:14.473", "mi_style": null, "mi_prinx": 544}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575433.1519507194, 6224837.509240246]},"properties": {"vejnavn": "Molsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:27:04.7", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:27:04.7", "mi_style": null, "mi_prinx": 545}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574117.4563655023, 6222280.277207391]},"properties": {"vejnavn": "Ordrupvej", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:27:44.68", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:27:44.68", "mi_style": null, "mi_prinx": 547}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573592.798767967, 6222723.202258727]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "17", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:30:01.607", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:01.607", "mi_style": null, "mi_prinx": 548}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573852.0671252563, 6224259.847022587]},"properties": {"vejnavn": "Ringkøbingvej ", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:31:29.06", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:31:29.06", "mi_style": null, "mi_prinx": 549}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573851.8617864472, 6224258.409650924]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:31:45.1", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:31:45.1", "mi_style": null, "mi_prinx": 550}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574375.1585652019, 6222033.415963625]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:33:45.17", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:45.17", "mi_style": null, "mi_prinx": 551}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574606.2883778228, 6227299.677618067]},"properties": {"vejnavn": "Vorregårds Allé ", "husnnr": "103", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:34:59.943", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:34:59.943", "mi_style": null, "mi_prinx": 552}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575306.5234312108, 6225834.736901434]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "13", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:36:31.937", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:36:31.937", "mi_style": null, "mi_prinx": 553}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575306.9341088288, 6225832.478174535]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "13", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:36:49.883", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:36:49.883", "mi_style": null, "mi_prinx": 554}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573236.9384143031, 6220161.223442913]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:38:24.073", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:38:24.073", "mi_style": null, "mi_prinx": 555}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570728.6893018486, 6216766.413757699]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:42:34.943", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:42:34.943", "mi_style": null, "mi_prinx": 556}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570725.4038809041, 6216767.645790554]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:42:46.957", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:42:46.957", "mi_style": null, "mi_prinx": 557}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570683.72010267, 6216786.947638603]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:20.413", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:20.413", "mi_style": null, "mi_prinx": 558}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570684.5414579061, 6216790.438398357]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:30.79", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:30.79", "mi_style": null, "mi_prinx": 559}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570716.1636344974, 6216831.916837782]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:47.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:47.467", "mi_style": null, "mi_prinx": 560}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570716.5743121156, 6216834.996919917]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:57.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:57.08", "mi_style": null, "mi_prinx": 561}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575070.7447433264, 6223252.195071867]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "144", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:45:13.057", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:45:13.057", "mi_style": null, "mi_prinx": 562}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574936.4408418891, 6222987.421971251]},"properties": {"vejnavn": "Marselisborg Allé", "husnnr": "30b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:46:52.93", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:46:52.93", "mi_style": null, "mi_prinx": 563}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573702.7326710331, 6224158.784515839]},"properties": {"vejnavn": "Regenburgs Plads", "husnnr": "50", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:48:19.28", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:48:19.28", "mi_style": null, "mi_prinx": 564}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573619.7098357283, 6224188.62422998]},"properties": {"vejnavn": "Regenburgsgade", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:50:10.157", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:50:10.157", "mi_style": null, "mi_prinx": 565}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573603.5732238201, 6222298.034907595]},"properties": {"vejnavn": "Rudpolh Wulffs Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:51:55.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:51:55.07", "mi_style": null, "mi_prinx": 566}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.381326311, 6223149.484703504]},"properties": {"vejnavn": "st st blichers gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:53:08.913", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:02:57.243", "mi_style": null, "mi_prinx": 567}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574499.2238193017, 6224391.241273099]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:58:48.823", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:58:48.823", "mi_style": null, "mi_prinx": 568}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575248.5108008216, 6223656.206817246]},"properties": {"vejnavn": "Toldbodgade", "husnnr": "8b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:31:29.01", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:31:29.01", "mi_style": null, "mi_prinx": 569}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575613.2119507189, 6225969.438398356]},"properties": {"vejnavn": "Herluf Trolles gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:35:59.73", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:35:59.73", "mi_style": null, "mi_prinx": 571}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575459.9619507184, 6225872.322381929]},"properties": {"vejnavn": "Otte Ruds Gade", "husnnr": "38", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:36:48.51", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:36:48.51", "mi_style": null, "mi_prinx": 572}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572973.5414579054, 6219822.767967144]},"properties": {"vejnavn": "Rundhøj Alle", "husnnr": "129", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:47:28.83", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:47:28.83", "mi_style": null, "mi_prinx": 573}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573964.5795687882, 6225531.675564681]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "84", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:52:36.853", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:52:36.853", "mi_style": null, "mi_prinx": 574}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573962.7315195069, 6225530.443531826]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "84", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:52:50.743", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:52:50.743", "mi_style": null, "mi_prinx": 575}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573763.6589431843, 6225400.732252075]},"properties": {"vejnavn": "Peter Fabers Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:02:57.817", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:02:57.817", "mi_style": null, "mi_prinx": 576}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573569.8198767971, 6225558.664271047]},"properties": {"vejnavn": "Peter Fabers Vej", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:03:59.61", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:03:59.61", "mi_style": null, "mi_prinx": 577}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573689.2009378455, 6225304.105604249]},"properties": {"vejnavn": "Viggo Stuckenbergs Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:05:26.077", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:05:26.077", "mi_style": null, "mi_prinx": 578}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571906.5431211507, 6223317.124229979]},"properties": {"vejnavn": "Hnerik Hertz Vej", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:06:25.737", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:06:25.737", "mi_style": null, "mi_prinx": 579}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572263.050246407, 6223168.713552361]},"properties": {"vejnavn": "O.I. offersens Vej", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:07:10.387", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:07:10.387", "mi_style": null, "mi_prinx": 580}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571635.578603885, 6220454.145748499]},"properties": {"vejnavn": "Grøndalsvej ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:06.363", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:06.363", "mi_style": null, "mi_prinx": 581}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571637.4266531663, 6220455.78845897]},"properties": {"vejnavn": "Grøndalsvej ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:17.097", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:17.097", "mi_style": null, "mi_prinx": 582}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571642.9708010103, 6220460.511251578]},"properties": {"vejnavn": "Grøndalsvej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:50.553", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:50.553", "mi_style": null, "mi_prinx": 583}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571645.8455443367, 6220461.948623242]},"properties": {"vejnavn": "Grøndalsvej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:11:00.837", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:11:00.837", "mi_style": null, "mi_prinx": 584}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575130.4157366346, 6223963.371970712]},"properties": {"vejnavn": "Fiskergade", "husnnr": "80", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:18:06.18", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:18:06.18", "mi_style": null, "mi_prinx": 585}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575127.8080963028, 6223964.838768398]},"properties": {"vejnavn": "Fiskergade", "husnnr": "80", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:18:18.71", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:18:18.71", "mi_style": null, "mi_prinx": 586}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575089.0245238632, 6223834.6749242265]},"properties": {"vejnavn": "Østergade", "husnnr": "48", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:19:11.287", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:19:11.287", "mi_style": null, "mi_prinx": 587}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574258.2222792607, 6222755.437371662]},"properties": {"vejnavn": "F. Vestergaards gade", "husnnr": "4", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:20:02.303", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:20:02.303", "mi_style": null, "mi_prinx": 588}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574263.5610882957, 6222709.852156057]},"properties": {"vejnavn": "F. Vestergaards Gade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:20:27.033", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:20:27.033", "mi_style": null, "mi_prinx": 589}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573703.2468583164, 6225533.67761807]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej ", "husnnr": "37", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 13:08:01.087", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 13:08:01.087", "mi_style": null, "mi_prinx": 590}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.4344056073, 6222520.606665064]},"properties": {"vejnavn": "Adoplh Meyers Vej", "husnnr": "10", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:06:23.613", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:06:23.613", "mi_style": null, "mi_prinx": 591}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573440.4595687881, 6220071.08932238]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:10:59.32", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:10:59.32", "mi_style": null, "mi_prinx": 592}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574108.964579056, 6219523.614989731]},"properties": {"vejnavn": "Niels Bjerres Vej", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:54:28.78", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:54:28.78", "mi_style": null, "mi_prinx": 593}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574773.7705954819, 6222620.240246407]},"properties": {"vejnavn": "Assensgade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 15:09:24.753", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 15:09:24.753", "mi_style": null, "mi_prinx": 594}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573563.520197629, 6225599.931539235]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "65", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-03-03 15:31:57.61", "rettet_af": "spatial_reader", "rettet_dato": "2025-03-03 15:31:57.61", "mi_style": null, "mi_prinx": 595}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573565.9366160922, 6225599.931539235]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "65", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-03-03 15:32:11.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-03-03 15:32:11.45", "mi_style": null, "mi_prinx": 596}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573900.4239732762, 6224444.639830079]},"properties": {"vejnavn": "Eugen Warmings Vej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 12:35:23.093", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:35:23.093", "mi_style": null, "mi_prinx": 597}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573895.0856880753, 6224474.736713882]},"properties": {"vejnavn": "Eugen Warmings Vej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 12:36:37.753", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:36:37.753", "mi_style": null, "mi_prinx": 598}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574904.906065573, 6222700.202576111]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 13:10:47.673", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 13:10:47.673", "mi_style": null, "mi_prinx": 599}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576675.4374502097, 6227137.355086146]},"properties": {"vejnavn": "stationsgade", "husnnr": "26", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 09:34:00.233", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:34:00.233", "mi_style": null, "mi_prinx": 600}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574471.4348262089, 6222940.82387054]},"properties": {"vejnavn": "Skt Anna Gade", "husnnr": null, "invalidepladser": null, "bemrk": "42", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 14:14:27.8", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 14:14:27.8", "mi_style": null, "mi_prinx": 601}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574477.5688115384, 6222914.615024131]},"properties": {"vejnavn": "Skt Anna Gade", "husnnr": "42", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 14:14:41.96", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 14:14:41.96", "mi_style": null, "mi_prinx": 602}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575230.8720257445, 6224047.548393872]},"properties": {"vejnavn": "Kannikegade 16", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 09:56:55.567", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:56:55.567", "mi_style": null, "mi_prinx": 603}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571983.2489361251, 6223205.13326127]},"properties": {"vejnavn": "Henrik Hertz Vej ", "husnnr": "72", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 10:59:53.147", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:59:53.147", "mi_style": null, "mi_prinx": 604}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571985.1224724249, 6223202.557148858]},"properties": {"vejnavn": "Henrik Hertz Vej", "husnnr": "72", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 11:01:03.507", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 11:01:03.507", "mi_style": null, "mi_prinx": 605}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574835.1320140511, 6222779.284543324]},"properties": {"vejnavn": "Hans Broges Gade", "husnnr": "17", "invalidepladser": 2, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 11:15:39.89", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 11:15:39.89", "mi_style": null, "mi_prinx": 606}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575250.902172792, 6224568.6143739]},"properties": {"vejnavn": "Studsgade ", "husnnr": "44", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 13:48:01.11", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 13:48:01.11", "mi_style": null, "mi_prinx": 607}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574412.2255140772, 6224274.489827803]},"properties": {"vejnavn": "Janus La Cours gade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:40:20.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:40:20.08", "mi_style": null, "mi_prinx": 608}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574548.0404080742, 6226415.892238429]},"properties": {"vejnavn": "Norges Allé ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:43:11.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:43:11.467", "mi_style": null, "mi_prinx": 609}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573738.9391065245, 6225498.3024416845]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:44:27.91", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:44:27.91", "mi_style": null, "mi_prinx": 610}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573740.2607020313, 6225496.834002232]},"properties": {"vejnavn": "Vilhelm Bergsøes vej", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:44:55.213", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:44:55.213", "mi_style": null, "mi_prinx": 611}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572487.2086247086, 6221123.172494171]},"properties": {"vejnavn": "Rørdamsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:47:21.347", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:47:21.347", "mi_style": null, "mi_prinx": 612}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572485.3438228437, 6221125.736596736]},"properties": {"vejnavn": "Rørdamsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:47:40.887", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:47:40.887", "mi_style": null, "mi_prinx": 613}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573450.7706904352, 6221551.299040821]},"properties": {"vejnavn": "H.C. Ørsteds Vej", "husnnr": "3b", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:49:03.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:49:03.467", "mi_style": null, "mi_prinx": 614}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574422.5215850822, 6227269.3811188815]},"properties": {"vejnavn": "Skrydstrupvej", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:50:15.83", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:50:15.83", "mi_style": null, "mi_prinx": 615}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574201.3469197123, 6222224.453960875]},"properties": {"vejnavn": "Ellemarksvej ", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:51:52.827", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:51:52.827", "mi_style": null, "mi_prinx": 616}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574261.9134668851, 6223538.282346269]},"properties": {"vejnavn": "Eckersbergsgade", "husnnr": "30", "invalidepladser": 1, "bemrk": "1345", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-12-02 13:14:47.253", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 13:14:47.253", "mi_style": null, "mi_prinx": 617}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572304.1753244308, 6223126.011642109]},"properties": {"vejnavn": "O. I. Offersensvej ", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 12:51:35.733", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 12:51:35.733", "mi_style": null, "mi_prinx": 618}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574370.9164150944, 6222348.226415094]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 14:21:24.417", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 14:21:24.417", "mi_style": null, "mi_prinx": 619}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574625.108490566, 6226140.433962264]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 14:22:33.083", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 14:22:33.083", "mi_style": null, "mi_prinx": 620}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575667.7509942062, 6225512.529924879]},"properties": {"vejnavn": "Trøjborgvej", "husnnr": "2b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-03 15:32:27.96", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-03 15:32:27.96", "mi_style": null, "mi_prinx": 621}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575717.5020655771, 6225812.445111568]},"properties": {"vejnavn": "Otte Ruds Gade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-10 15:36:13.387", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-10 15:36:13.387", "mi_style": null, "mi_prinx": 622}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574461.6616818061, 6226572.319186298]},"properties": {"vejnavn": "Stockholmsgade", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-10 16:10:04.953", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-10 16:10:04.953", "mi_style": null, "mi_prinx": 623}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575466.6230287044, 6226492.72476525]},"properties": {"vejnavn": "Skovfaldet", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-24 13:49:38.32", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-24 13:49:38.32", "mi_style": null, "mi_prinx": 624}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574060.9656603774, 6222480.358490566]},"properties": {"vejnavn": "Hans Schourups Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-04-14 13:45:00.16", "rettet_af": "spatial_reader", "rettet_dato": "2026-04-14 13:45:00.16", "mi_style": null, "mi_prinx": 626}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574258.8724698942, 6211072.413328694]},"properties": {"vejnavn": "Dampmøllevej", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:50:52.313", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:50:52.313", "mi_style": null, "mi_prinx": 628}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574259.8084374957, 6211070.354199971]},"properties": {"vejnavn": "Dampmøllevej ", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:51:13.877", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:51:13.877", "mi_style": null, "mi_prinx": 629}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572717.6960377358, 6224879.641509433]},"properties": {"vejnavn": "Præstevamgsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:52:51.823", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:52:51.823", "mi_style": null, "mi_prinx": 630}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573286.2358490566, 6225834.424528301]},"properties": {"vejnavn": "Majdalen", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:54:06.53", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:54:06.53", "mi_style": null, "mi_prinx": 631}}]} +{ "type": "FeatureCollection","crs": {"type": "name", "properties": {"name": "EPSG:25832"}}, "bbox": [570415.7999999999,6211070.354199971,577384.3500328106,6232262.860918405],"features": [{"type": "Feature","geometry": {"type": "Point","coordinates": [575153.9524951308, 6224260.609753487]},"properties": {"vejnavn": "Domkirkeplads/Bispegade", "husnnr": "1", "invalidepladser": 6, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:23.91", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:23.91", "mi_style": null, "mi_prinx": 172}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574656.6043964146, 6223886.745852402]},"properties": {"vejnavn": "Christiansgade", "husnnr": "2", "invalidepladser": 5, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:23.943", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:23.943", "mi_style": null, "mi_prinx": 173}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574519.3323188772, 6223394.810052871]},"properties": {"vejnavn": "Krigersvej", "husnnr": "5", "invalidepladser": 4, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:04:59.57", "mi_style": null, "mi_prinx": 175}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574916.9504383737, 6223961.870560985]},"properties": {"vejnavn": "Telefonsmøgen / Busgaden", "husnnr": "6", "invalidepladser": 3, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.113", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.113", "mi_style": null, "mi_prinx": 176}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574742.8394360626, 6224182.215710367]},"properties": {"vejnavn": "Grønnegade", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.15", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.15", "mi_style": null, "mi_prinx": 177}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574727.5186573606, 6224387.563912335]},"properties": {"vejnavn": "Klostergade 1", "husnnr": "74", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.193", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.193", "mi_style": null, "mi_prinx": 178}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574855.8060164271, 6224401.099589322]},"properties": {"vejnavn": "Klostergade 2", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.26", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:04.027", "mi_style": null, "mi_prinx": 179}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574504.0892603148, 6223553.9135994455]},"properties": {"vejnavn": "Skt. Nicolaus Gade", "husnnr": "1 til 3", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.363", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.363", "mi_style": null, "mi_prinx": 181}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574952.5463860369, 6223535.318275151]},"properties": {"vejnavn": "Ny Banegårdsgade", "husnnr": "49", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.403", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:16:08.26", "mi_style": null, "mi_prinx": 182}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575228.2588088225, 6224049.9438427165]},"properties": {"vejnavn": "Kannikegade", "husnnr": "16A", "invalidepladser": 2, "bemrk": "Tidsbegrænset", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.46", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:57:11.063", "mi_style": null, "mi_prinx": 183}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575319.8423906788, 6224763.017489845]},"properties": {"vejnavn": "Nørreport", "husnnr": "22", "invalidepladser": 2, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.51", "mi_style": null, "mi_prinx": 184}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574890.2916237647, 6223681.297670785]},"properties": {"vejnavn": "Rosenkrantzgade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.557", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 14:37:23.03", "mi_style": null, "mi_prinx": 185}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574751.5470476393, 6223649.994042548]},"properties": {"vejnavn": "Park Alle 2", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.61", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.61", "mi_style": null, "mi_prinx": 186}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575067.4865718425, 6224480.395016832]},"properties": {"vejnavn": "Borggade 1", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.657", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.657", "mi_style": null, "mi_prinx": 187}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574636.8473534812, 6223433.838040478]},"properties": {"vejnavn": "Orla Lehmanns Alle", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.743", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.743", "mi_style": null, "mi_prinx": 188}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574714.8, 6223554.750000001]},"properties": {"vejnavn": "Johannes Bjergs Gade", "husnnr": "", "invalidepladser": 1, "bemrk": "På hjørner ved Banegårdspladsen 14", "ident": "1185", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.773", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:49:33.863", "mi_style": null, "mi_prinx": 189}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574779.9704729278, 6223780.417495232]},"properties": {"vejnavn": "Rådhuspladsen", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.81", "mi_style": null, "mi_prinx": 190}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575021.1449268986, 6223906.111910498]},"properties": {"vejnavn": "Ferdinand Sallings Stræde", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.847", "mi_style": null, "mi_prinx": 191}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575028.063190263, 6223924.808104941]},"properties": {"vejnavn": "Ferdinand Sallings Stræde", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.873", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.873", "mi_style": null, "mi_prinx": 192}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574539.4144572791, 6224578.814983732]},"properties": {"vejnavn": "Grønnegade", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.907", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.907", "mi_style": null, "mi_prinx": 193}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574533.8732499122, 6224584.343858345]},"properties": {"vejnavn": "Grønnegade", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.933", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:24.933", "mi_style": null, "mi_prinx": 194}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575460.8322248239, 6224695.803278689]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:24.97", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:10:54.057", "mi_style": null, "mi_prinx": 195}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575461.5348009364, 6224697.442622952]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.003", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:10:30.637", "mi_style": null, "mi_prinx": 196}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575455.3216124629, 6226039.447676172]},"properties": {"vejnavn": "Steen Billes Torv", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.037", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.037", "mi_style": null, "mi_prinx": 197}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573959.1296566332, 6223167.542244217]},"properties": {"vejnavn": "Ankersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.07", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.07", "mi_style": null, "mi_prinx": 198}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573965.1903521908, 6223169.331879942]},"properties": {"vejnavn": "Ankersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.093", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.093", "mi_style": null, "mi_prinx": 199}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573771.8681415829, 6222925.054833357]},"properties": {"vejnavn": "Læssøesgade", "husnnr": "85", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.127", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:14:58.7", "mi_style": null, "mi_prinx": 200}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573866.0093369979, 6222685.91027901]},"properties": {"vejnavn": "Harald Jenesens Plads", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.163", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.163", "mi_style": null, "mi_prinx": 201}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573940.9310782718, 6222759.405319308]},"properties": {"vejnavn": "Harald Jenesens Plads", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.19", "mi_style": null, "mi_prinx": 202}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574372.5907076919, 6222033.218436124]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.297", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:25.34", "mi_style": null, "mi_prinx": 203}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574368.4426301755, 6222031.638216118]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.323", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:16.533", "mi_style": null, "mi_prinx": 204}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574370.6154326841, 6222032.23079862]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.35", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:06.723", "mi_style": null, "mi_prinx": 205}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574136.7129774133, 6222538.070841889]},"properties": {"vejnavn": "Chr. Wærums Vej", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.387", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:47:59.483", "mi_style": null, "mi_prinx": 206}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574736.8941823252, 6222561.355631749]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.427", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.427", "mi_style": null, "mi_prinx": 207}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574086.8495389346, 6223044.81722454]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.5", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.5", "mi_style": null, "mi_prinx": 209}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575326.9832918391, 6224757.308651863]},"properties": {"vejnavn": "Nørreport", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.53", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.53", "mi_style": null, "mi_prinx": 210}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575469.2735810118, 6224466.787786544]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.567", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.567", "mi_style": null, "mi_prinx": 211}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575471.3432879301, 6224465.178114189]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.59", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.59", "mi_style": null, "mi_prinx": 212}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575473.4129948484, 6224463.568441832]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.617", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.617", "mi_style": null, "mi_prinx": 213}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575475.4744559224, 6224461.968767442]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.647", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.647", "mi_style": null, "mi_prinx": 214}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575477.5441628407, 6224460.129141892]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.673", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.673", "mi_style": null, "mi_prinx": 215}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575479.1521024784, 6224458.289516343]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.773", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.773", "mi_style": null, "mi_prinx": 216}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575481.2218093968, 6224456.219937599]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.797", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.797", "mi_style": null, "mi_prinx": 217}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575483.0523868304, 6224454.620263209]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.823", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.823", "mi_style": null, "mi_prinx": 218}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575485.1220937488, 6224452.320731272]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.85", "mi_style": null, "mi_prinx": 219}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575487.1918006671, 6224449.791246141]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.88", "mi_style": null, "mi_prinx": 220}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575515.433817381, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.907", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.907", "mi_style": null, "mi_prinx": 221}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575517.96529158, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.93", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.93", "mi_style": null, "mi_prinx": 222}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575520.9502872152, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.957", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.957", "mi_style": null, "mi_prinx": 223}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575523.9352828503, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:25.983", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:25.983", "mi_style": null, "mi_prinx": 224}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575526.9202784855, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.007", "mi_style": null, "mi_prinx": 225}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575529.6743904804, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.033", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.033", "mi_style": null, "mi_prinx": 226}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575532.6593861155, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.067", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.067", "mi_style": null, "mi_prinx": 227}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575535.4217439546, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.09", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.09", "mi_style": null, "mi_prinx": 228}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575538.4067395899, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.117", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.117", "mi_style": null, "mi_prinx": 229}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575540.9299679445, 6224381.585129299]},"properties": {"vejnavn": "Inge Lehmanns Gade", "husnnr": null, "invalidepladser": 1, "bemrk": "Navitas", "ident": "P", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.157", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.157", "mi_style": null, "mi_prinx": 230}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574394.576202219, 6223499.304714926]},"properties": {"vejnavn": "Valdemarsgade", "husnnr": "20", "invalidepladser": 1, "bemrk": null, "ident": "1102", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.19", "mi_style": null, "mi_prinx": 231}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575033.2828097024, 6222970.362379608]},"properties": {"vejnavn": "Heibergsgade", "husnnr": "27", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.223", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.223", "mi_style": null, "mi_prinx": 232}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573499.0, 6224197.1000000015]},"properties": {"vejnavn": "Regensburgsgade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.257", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:24:06.55", "mi_style": null, "mi_prinx": 233}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574459.3308174759, 6223990.544724442]},"properties": {"vejnavn": "Marstrandsgade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1104", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.287", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.287", "mi_style": null, "mi_prinx": 234}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574218.7335726058, 6224300.371660066]},"properties": {"vejnavn": "Mønsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.32", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.32", "mi_style": null, "mi_prinx": 235}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574841.6081590408, 6224887.112230719]},"properties": {"vejnavn": "Sjællandsgade", "husnnr": "92", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.353", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.353", "mi_style": null, "mi_prinx": 236}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224497.9]},"properties": {"vejnavn": "Tage Hansens Gade", "husnnr": "31B", "invalidepladser": 1, "bemrk": null, "ident": "1108", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.38", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:18.623", "mi_style": null, "mi_prinx": 237}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573350.8007395364, 6225946.216653077]},"properties": {"vejnavn": "Hammershusvej", "husnnr": "41", "invalidepladser": 1, "bemrk": "Hammershusvej mellem nr. 41 og nr. 43", "ident": "1109", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.413", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.413", "mi_style": null, "mi_prinx": 238}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575124.3828974852, 6225601.696779101]},"properties": {"vejnavn": "Skt. Johannes Allé", "husnnr": "4", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": "1110", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.443", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:00:33.773", "mi_style": null, "mi_prinx": 239}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575252.1357631639, 6224570.226731846]},"properties": {"vejnavn": "Studsgade", "husnnr": "44", "invalidepladser": 2, "bemrk": null, "ident": "1111", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.473", "mi_style": null, "mi_prinx": 240}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574824.2671484861, 6226340.746347698]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "58", "invalidepladser": 1, "bemrk": null, "ident": "1111", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.507", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.507", "mi_style": null, "mi_prinx": 241}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575281.3650102671, 6223652.100041065]},"properties": {"vejnavn": "Toldbodgade", "husnnr": "6", "invalidepladser": 1, "bemrk": "Ud for svømmehallen", "ident": "1112", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.537", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:31:09.807", "mi_style": null, "mi_prinx": 242}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573871.8803781366, 6222609.255881792]},"properties": {"vejnavn": "Carl Bertelsens Gade", "husnnr": "3", "invalidepladser": 1, "bemrk": "I nærheden af Carl Bertelsens Gade nr. 3", "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.567", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.567", "mi_style": null, "mi_prinx": 243}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574261.0265080002, 6223960.050931365]},"properties": {"vejnavn": "Jerichausgade", "husnnr": "24", "invalidepladser": 1, "bemrk": "I den eksisterende skråparkering ud for nr. 24", "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.657", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.657", "mi_style": null, "mi_prinx": 245}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573354.775236487, 6224149.5423609335]},"properties": {"vejnavn": "Silkeborgvej", "husnnr": "108", "invalidepladser": 1, "bemrk": null, "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.717", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.717", "mi_style": null, "mi_prinx": 246}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574302.3464337677, 6219824.742661404]},"properties": {"vejnavn": "Kridthøjtorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "1113", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.763", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.763", "mi_style": null, "mi_prinx": 247}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574881.526291278, 6222643.79885067]},"properties": {"vejnavn": "Assensgade", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1114", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:26.92", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:26.92", "mi_style": null, "mi_prinx": 248}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574120.6245171713, 6222766.663841857]},"properties": {"vejnavn": "Ingerslev Boulevard", "husnnr": "19", "invalidepladser": 1, "bemrk": "I nærheden af Ingerslev Boulevard nr. 19", "ident": "1114", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.0", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.0", "mi_style": null, "mi_prinx": 250}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574835.6134302375, 6222775.322079497]},"properties": {"vejnavn": "Hans Broges Gade", "husnnr": "17", "invalidepladser": 2, "bemrk": "Borger ønsker pladsen tidsbegrænset, da det er ved arbejdspladsen", "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.083", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.083", "mi_style": null, "mi_prinx": 252}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575090.5666900271, 6223833.916605643]},"properties": {"vejnavn": "Østergade", "husnnr": "35", "invalidepladser": 1, "bemrk": null, "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.143", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.143", "mi_style": null, "mi_prinx": 253}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575371.6015553251, 6224793.341317518]},"properties": {"vejnavn": "Helgenæsgade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.187", "mi_style": null, "mi_prinx": 254}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574443.9523178637, 6224528.715181402]},"properties": {"vejnavn": "Sejrøgade", "husnnr": "11", "invalidepladser": 1, "bemrk": "I nærheden af Sejrøgade nr.11", "ident": "1116", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.243", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.243", "mi_style": null, "mi_prinx": 255}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575462.4460219346, 6224699.380442982]},"properties": {"vejnavn": "Kystvejen", "husnnr": "55", "invalidepladser": 3, "bemrk": "Den ene af tre eksisterende HC-pladser ud for nr. 53-55 tidsbegrænset fra kl.8.00-14.00", "ident": "1118", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.29", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.29", "mi_style": null, "mi_prinx": 256}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572618.3471282512, 6224015.439657151]},"properties": {"vejnavn": "Silkeborgvej", "husnnr": "226", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.343", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.343", "mi_style": null, "mi_prinx": 257}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572934.9133366208, 6219773.593072755]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "137", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.393", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.393", "mi_style": null, "mi_prinx": 258}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572966.1156114371, 6219796.788351424]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "135", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.453", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.453", "mi_style": null, "mi_prinx": 259}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573958.7998228613, 6222500.128094437]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.513", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.513", "mi_style": null, "mi_prinx": 260}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574108.2557507273, 6222343.6199512165]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "2", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.57", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.57", "mi_style": null, "mi_prinx": 261}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574964.7928269794, 6222704.936406298]},"properties": {"vejnavn": "Strandparken", "husnnr": "36", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.613", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.613", "mi_style": null, "mi_prinx": 262}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574986.6525602082, 6222771.112936256]},"properties": {"vejnavn": "Strandparken", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.65", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.65", "mi_style": null, "mi_prinx": 263}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574174.5193554903, 6222819.103167984]},"properties": {"vejnavn": "Ingerslevs Boulevard", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.7", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.7", "mi_style": null, "mi_prinx": 264}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574259.3031265422, 6222874.021989416]},"properties": {"vejnavn": "Lundinsgade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.753", "mi_style": null, "mi_prinx": 265}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574732.0621175678, 6222787.729553992]},"properties": {"vejnavn": "Tietgens Plads", "husnnr": "11", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.81", "mi_style": null, "mi_prinx": 266}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574820.7791563489, 6223119.082108137]},"properties": {"vejnavn": "Brammersgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.857", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.857", "mi_style": null, "mi_prinx": 267}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574401.956232864, 6223179.749759412]},"properties": {"vejnavn": "Godthåbsgade", "husnnr": null, "invalidepladser": 1, "bemrk": "Overfor nr. 2", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:27.963", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:27.963", "mi_style": null, "mi_prinx": 268}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574564.0530400358, 6223225.930359485]},"properties": {"vejnavn": "Dybbølsgade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.01", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.01", "mi_style": null, "mi_prinx": 269}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574140.167168153, 6223217.512073003]},"properties": {"vejnavn": "Absolonsgade", "husnnr": "28", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.043", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.043", "mi_style": null, "mi_prinx": 270}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574275.7206025358, 6223430.478724257]},"properties": {"vejnavn": "Sonnesgade", "husnnr": "7", "invalidepladser": 3, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.083", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.083", "mi_style": null, "mi_prinx": 271}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574256.1861973983, 6223461.63238302]},"properties": {"vejnavn": "Echersbergsgade", "husnnr": "43", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.15", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.15", "mi_style": null, "mi_prinx": 273}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574374.357392005, 6223467.071275949]},"properties": {"vejnavn": "Valdemarsgade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.187", "mi_style": null, "mi_prinx": 274}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574769.5764453339, 6223465.5386415245]},"properties": {"vejnavn": "Banegårdspladsen", "husnnr": "17", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.223", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:53:06.79", "mi_style": null, "mi_prinx": 275}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574924.5695985033, 6223516.741165786]},"properties": {"vejnavn": "Ny Banegårdsgade", "husnnr": "40", "invalidepladser": 2, "bemrk": "Overfor nr. 45", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.26", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.26", "mi_style": null, "mi_prinx": 276}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575241.6470492193, 6223772.769052051]},"properties": {"vejnavn": "Ridderstræde", "husnnr": null, "invalidepladser": 1, "bemrk": "Overfor nr 1", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.3", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.3", "mi_style": null, "mi_prinx": 277}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575132.5344444042, 6223962.394105587]},"properties": {"vejnavn": "Fiskergade", "husnnr": "84", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:17:48.05", "mi_style": null, "mi_prinx": 279}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574746.0553153381, 6224061.31032031]},"properties": {"vejnavn": "Christiansgade", "husnnr": "29S", "invalidepladser": 3, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.437", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.437", "mi_style": null, "mi_prinx": 281}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575181.3781732596, 6224118.438691995]},"properties": {"vejnavn": "Teatergaden", "husnnr": null, "invalidepladser": 3, "bemrk": "Sydlig endeaf vejen, på nordlig side", "ident": "E-8686", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.473", "mi_style": null, "mi_prinx": 282}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575222.3270360337, 6224141.414015435]},"properties": {"vejnavn": "Teatergaden", "husnnr": null, "invalidepladser": 4, "bemrk": "Nordlig ende af vejen , nordlig side", "ident": "E-8686", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.51", "mi_style": null, "mi_prinx": 283}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.7847335092, 6224144.713343865]},"properties": {"vejnavn": "Grønnegade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.587", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.587", "mi_style": null, "mi_prinx": 285}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574634.6704505871, 6224881.683335755]},"properties": {"vejnavn": "Falstergade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.66", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.66", "mi_style": null, "mi_prinx": 287}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573664.0273809661, 6224806.278684154]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "12", "invalidepladser": 2, "bemrk": "Overfor nr. 39", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.703", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.703", "mi_style": null, "mi_prinx": 288}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573900.8397833044, 6224443.072613699]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.74", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.74", "mi_style": null, "mi_prinx": 289}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573876.0610211948, 6224382.904860672]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.777", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.777", "mi_style": null, "mi_prinx": 290}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573858.2335058267, 6224344.252728201]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "27", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.81", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.81", "mi_style": null, "mi_prinx": 291}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574637.7049212881, 6224717.846684231]},"properties": {"vejnavn": "Hjarnøgade", "husnnr": "5", "invalidepladser": 2, "bemrk": "Overfor nr 5", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.843", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.843", "mi_style": null, "mi_prinx": 292}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574863.3524504495, 6224285.3847106155]},"properties": {"vejnavn": "Vestergade", "husnnr": "22", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.88", "mi_style": null, "mi_prinx": 293}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574537.1138667205, 6224140.554190449]},"properties": {"vejnavn": "Møllegade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.917", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.917", "mi_style": null, "mi_prinx": 294}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575074.9490609304, 6224661.438166022]},"properties": {"vejnavn": "Anholtsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:28.963", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:28.963", "mi_style": null, "mi_prinx": 295}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572960.45896225, 6225756.165337472]},"properties": {"vejnavn": "Musvågevej", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.073", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:51:29.073", "mi_style": null, "mi_prinx": 296}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575340.1766427128, 6224351.371279241]},"properties": {"vejnavn": "Olufgade", "husnnr": "3", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.113", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:00:13.91", "mi_style": null, "mi_prinx": 297}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573871.0393020185, 6222888.768987707]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "gl", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:09:34.883", "mi_style": null, "mi_prinx": 298}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574914.8559939226, 6222594.038979148]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "34", "invalidepladser": 1, "bemrk": "5x3,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:19.733", "mi_style": null, "mi_prinx": 299}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575638.25, 6226153.625]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "222", "invalidepladser": 1, "bemrk": "5x3,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.26", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:56:12.1", "mi_style": null, "mi_prinx": 301}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574462.8600388346, 6226572.059264624]},"properties": {"vejnavn": "Stockholmsgade", "husnnr": "1", "invalidepladser": 1, "bemrk": "5x2,5", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.293", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:19.97", "mi_style": null, "mi_prinx": 302}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574497.212226172, 6224409.829380264]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "3", "invalidepladser": 1, "bemrk": "Til højre for porten", "ident": "1124", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.38", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.12", "mi_style": null, "mi_prinx": 304}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575140.6024732155, 6224850.239736008]},"properties": {"vejnavn": "Høegh Guldbergs Gade", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": "1130", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.423", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.197", "mi_style": null, "mi_prinx": 305}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574098.9791758942, 6222367.355119992]},"properties": {"vejnavn": "P. P. Ørumsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1127", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.467", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.267", "mi_style": null, "mi_prinx": 306}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573984.3949235562, 6222175.364199189]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": "1129", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.507", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.337", "mi_style": null, "mi_prinx": 307}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574502.2421911926, 6222857.645322839]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.543", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:03:19.153", "mi_style": null, "mi_prinx": 308}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574505.5487747552, 6222858.265196665]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.57", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:03:07.203", "mi_style": null, "mi_prinx": 309}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574509.0697502696, 6222858.8850704925]},"properties": {"vejnavn": "Ingerslev Torv", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.6", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 10:02:53.353", "mi_style": null, "mi_prinx": 310}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.586833246, 6222612.475226503]},"properties": {"vejnavn": "Assensgade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": "1121", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.633", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.67", "mi_style": null, "mi_prinx": 311}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574719.5036967049, 6222608.076121928]},"properties": {"vejnavn": "Assensgade", "husnnr": "21", "invalidepladser": 2, "bemrk": null, "ident": "1128", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.677", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.737", "mi_style": null, "mi_prinx": 312}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576416.2510775031, 6232254.552609537]},"properties": {"vejnavn": "Lystruplund", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for børnehavens indgang", "ident": "1123", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.717", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.81", "mi_style": null, "mi_prinx": 313}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573121.6899557706, 6220138.458805391]},"properties": {"vejnavn": "Rundhøj Alle", "husnnr": "61", "invalidepladser": 1, "bemrk": null, "ident": "1133", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.883", "mi_style": null, "mi_prinx": 314}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576412.5899226356, 6232262.860918405]},"properties": {"vejnavn": "Lystruplund", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for børnehavens indgang", "ident": "1123", "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.783", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:20.953", "mi_style": null, "mi_prinx": 315}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575079.1956707429, 6224575.955565759]},"properties": {"vejnavn": "Borggade", "husnnr": "16", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.86", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.13", "mi_style": null, "mi_prinx": 317}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574136.9018138117, 6222534.351128438]},"properties": {"vejnavn": "Chr. Wærumsgade", "husnnr": "29", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:29.943", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.277", "mi_style": null, "mi_prinx": 319}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573894.8368086569, 6224475.935924468]},"properties": {"vejnavn": "Eugen Warmingsvej", "husnnr": "6", "invalidepladser": 2, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.347", "mi_style": null, "mi_prinx": 320}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576295.1526081712, 6228305.306467168]},"properties": {"vejnavn": "Fortebakken", "husnnr": "34A", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.047", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.417", "mi_style": null, "mi_prinx": 321}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575393.7251555714, 6223907.781570643]},"properties": {"vejnavn": "Hack Kampmanns plads", "husnnr": "2", "invalidepladser": 3, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.09", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.487", "mi_style": null, "mi_prinx": 322}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574911.6071312699, 6222880.060760242]},"properties": {"vejnavn": "Hendrik Pontopidans gade", "husnnr": "2", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.123", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.56", "mi_style": null, "mi_prinx": 323}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575107.1408370622, 6226017.862069859]},"properties": {"vejnavn": "Herambsgade", "husnnr": "5", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.16", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.6", "mi_style": null, "mi_prinx": 324}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575153.6061696704, 6211381.751207897]},"properties": {"vejnavn": "Mallingsgårdsvej", "husnnr": "137", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.19", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.65", "mi_style": null, "mi_prinx": 325}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573888.9575216738, 6222869.90282786]},"properties": {"vejnavn": "Max Mullers Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.227", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.71", "mi_style": null, "mi_prinx": 326}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574546.3132659666, 6223169.7029371355]},"properties": {"vejnavn": "Montaangade", "husnnr": "8", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.263", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:58:27.443", "mi_style": null, "mi_prinx": 327}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573574.4692660668, 6222862.3343683975]},"properties": {"vejnavn": "Nordborg Gade", "husnnr": "41", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.3", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:21.947", "mi_style": null, "mi_prinx": 328}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574323.8351040032, 6223091.617698438]},"properties": {"vejnavn": "Ole Rømers Gade", "husnnr": "38", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.333", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.02", "mi_style": null, "mi_prinx": 329}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574564.1684818559, 6223099.706052078]},"properties": {"vejnavn": "Ole Rømers Gade", "husnnr": "63", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.373", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.09", "mi_style": null, "mi_prinx": 330}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573623.1032557249, 6222316.575456046]},"properties": {"vejnavn": "Rudolp Wulffs Gade", "husnnr": "9", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.17", "mi_style": null, "mi_prinx": 331}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572704.5079553006, 6226851.832317628]},"properties": {"vejnavn": "Sandkåsvej", "husnnr": "27", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.22", "mi_style": null, "mi_prinx": 332}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574883.422835466, 6227413.9079087665]},"properties": {"vejnavn": "Solhvervsvej", "husnnr": "22", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.547", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.29", "mi_style": null, "mi_prinx": 333}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574839.5219604338, 6224676.74505035]},"properties": {"vejnavn": "Thunøgade", "husnnr": "21", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.59", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:26:13.553", "mi_style": null, "mi_prinx": 334}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574010.542495819, 6222959.064679222]},"properties": {"vejnavn": "Trepkasgade", "husnnr": "6", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.64", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.437", "mi_style": null, "mi_prinx": 335}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573839.0454261499, 6225405.03680867]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "78", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.68", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.533", "mi_style": null, "mi_prinx": 336}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574665.6000000001, 6227253.1000000015]},"properties": {"vejnavn": "Vorregårds Allé", "husnnr": "95", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.717", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:50:45.603", "mi_style": null, "mi_prinx": 337}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574079.8900463488, 6222655.326504249]},"properties": {"vejnavn": "Ålborgade", "husnnr": "30", "invalidepladser": 1, "bemrk": "Efter henvenddelse fra Pia Møller", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.753", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.66", "mi_style": null, "mi_prinx": 338}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575257.8748707939, 6225740.82845925]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "86", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.79", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.73", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 339}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575579.6936819801, 6225552.636765125]},"properties": {"vejnavn": "Trøjborgvej", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud for Kochs Skole", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.86", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.893", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 341}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575354.5079200993, 6225296.238954158]},"properties": {"vejnavn": "Kirkegårdsvej", "husnnr": "53", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.9", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:22.95", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 342}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575673.3747190277, 6225441.769331871]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44B", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:30.953", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.047", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 343}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575589.1104361663, 6225071.994598448]},"properties": {"vejnavn": "Skovvejen", "husnnr": "19B", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.007", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.12", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 344}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575246.1905094264, 6225228.322778299]},"properties": {"vejnavn": "Peter Sabroes Gade", "husnnr": "2-4", "invalidepladser": 10, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.04", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.21", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 345}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575188.106782205, 6225234.641492143]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 3, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.08", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.3", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 346}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575106.3244984768, 6225400.537724445]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 4, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.117", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.37", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 347}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575064.3119217885, 6225293.259560605]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.16", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.437", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 348}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575109.6475737281, 6225313.485443685]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.193", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.503", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 349}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575068.0720267874, 6225440.379614743]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.223", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.567", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 350}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.441055486, 6225637.589473247]},"properties": {"vejnavn": "Hospitalsgaden", "husnnr": null, "invalidepladser": 5, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.273", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.637", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 351}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574422.6368103585, 6225613.944286156]},"properties": {"vejnavn": "Victor Albecks Vej", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.303", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.71", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 352}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574900.5989291347, 6225286.400956654]},"properties": {"vejnavn": "Vennelyst Boulevard", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.34", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.78", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 353}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574878.0960200508, 6225366.764598867]},"properties": {"vejnavn": "Bartholins Allé", "husnnr": null, "invalidepladser": 1, "bemrk": "V/Bartholinbygningen", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.373", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.847", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 354}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574170.8004797128, 6225423.143123182]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.41", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:23.913", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 355}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575117.8851721799, 6224865.706587776]},"properties": {"vejnavn": "Høegh-Guldbergs Gade", "husnnr": "49", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.473", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.033", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 357}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574990.5281070274, 6224945.140419249]},"properties": {"vejnavn": "Høegh-Guldbergs Gade", "husnnr": "6A", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.51", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.103", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 358}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574350.6093604325, 6224544.851896821]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "41", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.597", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.153", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 359}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574519.1874012209, 6224508.419312567]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.63", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.28", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 360}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574525.4130136644, 6224532.194473202]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.667", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.357", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 361}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573850.8204918046, 6224325.876468635]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.757", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.57", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 364}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573842.8467603703, 6224172.757635531]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.787", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.62", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 365}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573685.0130546996, 6224142.763740702]},"properties": {"vejnavn": "Regenburgs Plads", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.817", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.683", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 366}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573708.2663356145, 6223856.911925012]},"properties": {"vejnavn": "Åparken", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.847", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.737", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 367}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573789.7847523252, 6223920.838912859]},"properties": {"vejnavn": "Åparken", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.88", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.803", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 368}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574850.3899832161, 6222925.211569925]},"properties": {"vejnavn": "Odensegade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.923", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.9", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 369}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574954.056737706, 6223018.042674422]},"properties": {"vejnavn": "Marselisborg Allé", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.953", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:24.97", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 370}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574731.5426293771, 6223104.335109847]},"properties": {"vejnavn": "Sankt Pauls Gade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:31.987", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.04", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 371}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574744.3401797245, 6223104.785018269]},"properties": {"vejnavn": "Sankt Pauls Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.027", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.113", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 372}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574725.6138673283, 6222733.070679648]},"properties": {"vejnavn": "J. Michaelsens Gade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.123", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.31", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 375}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574543.1745622781, 6223191.127443518]},"properties": {"vejnavn": "Montanagade", "husnnr": "4A", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.153", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.35", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 376}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574802.1600399285, 6223111.60363036]},"properties": {"vejnavn": "Brammersgade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.187", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.393", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 377}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574870.3036971911, 6223125.000903384]},"properties": {"vejnavn": "Brammersgade", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.217", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.43", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 378}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.7603455347, 6223146.6087693]},"properties": {"vejnavn": "St. St. Blichers Gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:03:17.84", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 379}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574131.5007857978, 6222588.660074009]},"properties": {"vejnavn": "Chr. Wærums Gade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.293", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.507", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 380}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574200.5290566034, 6222565.141509435]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.327", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-24 13:56:15.127", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 381}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574191.027535771, 6222648.197955245]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.36", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.58", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 382}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574193.9630563403, 6222718.033740373]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.39", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.617", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 383}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575232.5353912722, 6224544.841898856]},"properties": {"vejnavn": "Studsgade", "husnnr": "38", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.423", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.673", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 384}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574716.4527343153, 6224380.4153674]},"properties": {"vejnavn": "Klostergade", "husnnr": "78", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.457", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.71", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 385}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574748.306430831, 6224145.5731688505]},"properties": {"vejnavn": "Grønnegade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.497", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.747", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 386}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574797.3197293266, 6224474.496217515]},"properties": {"vejnavn": "Gammel Munkegade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.863", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.827", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 388}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574841.4597338435, 6224276.456527921]},"properties": {"vejnavn": "Vestergade", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.897", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.863", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 389}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.8203643236, 6224302.431240844]},"properties": {"vejnavn": "Badstuegade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.927", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.9", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 390}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574934.1924987969, 6223330.958981213]},"properties": {"vejnavn": "M.P. Bruuns Gade", "husnnr": "25", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.957", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.933", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 391}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574915.3095153589, 6223254.954451715]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "124", "invalidepladser": 1, "bemrk": "8-16", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:32.997", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:25.97", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 392}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574915.6393491307, 6223263.812648655]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "97", "invalidepladser": 1, "bemrk": "8-16", "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.043", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.007", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 393}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574488.7107607027, 6223237.937915382]},"properties": {"vejnavn": "Istedgade", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.077", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.043", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 394}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574767.2962480201, 6223464.91676953]},"properties": {"vejnavn": "Banegårdspladsen", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.19", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:52:54.773", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 396}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574530.2173944163, 6223395.66940094]},"properties": {"vejnavn": "Kriegersvej", "husnnr": "11", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.22", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:04:44.057", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 397}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574906.5854120937, 6223675.0989325205]},"properties": {"vejnavn": "Rosenkrantzgade", "husnnr": "24", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 14:37:07.49", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 398}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575020.6317082599, 6224083.022794313]},"properties": {"vejnavn": "Fiskergade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.287", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:55:01.553", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 399}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574872.5218293066, 6223967.619390828]},"properties": {"vejnavn": "Telefonsmøgen", "husnnr": null, "invalidepladser": 3, "bemrk": null, "ident": null, "oprettet_af": "ADM\\aztnbnd", "oprettet_dato": "2019-03-19 14:51:33.327", "rettet_af": "ADM\\aztnbnd", "rettet_dato": "2019-03-19 14:52:26.6", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 400}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574997.9, 6222462.25]},"properties": {"vejnavn": "Adolph Meyers Vej", "husnnr": null, "invalidepladser": 1, "bemrk": "Ud mod Strandvejen", "ident": "1196", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-16 11:55:35.953", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-16 12:00:59.113", "mi_style": null, "mi_prinx": 404}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573880.2000000001, 6222878.7]},"properties": {"vejnavn": "Max Müllers Gade 11", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": "1169", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:08:44.613", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:08:44.613", "mi_style": null, "mi_prinx": 405}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573875.6000000001, 6222883.899999999]},"properties": {"vejnavn": "Max Müllers Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "1169", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:11:20.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:11:20.153", "mi_style": null, "mi_prinx": 406}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574233.0, 6226255.500000001]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "51", "invalidepladser": 1, "bemrk": null, "ident": "1171", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:14:12.3", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:14:12.3", "mi_style": null, "mi_prinx": 407}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573849.4, 6222996.65]},"properties": {"vejnavn": "Læssøegade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": "1173", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:18:31.19", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:18:31.19", "mi_style": null, "mi_prinx": 408}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574396.7, 6224597.35]},"properties": {"vejnavn": "Skt. Marcus Kirkeplads", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1173", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:21:54.81", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:21:54.81", "mi_style": null, "mi_prinx": 409}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574845.2999999999, 6224678.449999999]},"properties": {"vejnavn": "Thunøgade 26B", "husnnr": "26B", "invalidepladser": 1, "bemrk": null, "ident": "1176", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:25:55.553", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:25:55.553", "mi_style": null, "mi_prinx": 410}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573925.4, 6222166.899999999]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": "1181", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:31:40.443", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:31:40.443", "mi_style": null, "mi_prinx": 412}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571908.4, 6223314.500000001]},"properties": {"vejnavn": "Henrik Hertz Vej", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": "1182", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:34:51.437", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:34:51.437", "mi_style": null, "mi_prinx": 413}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574719.1, 6223555.250000001]},"properties": {"vejnavn": "Johannes Bjergs Gade", "husnnr": "", "invalidepladser": 1, "bemrk": "På hjørnet ved Banegårdspladsen 14", "ident": "1185", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:48:20.697", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:48:20.697", "mi_style": null, "mi_prinx": 416}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575397.9500000001, 6225939.025]},"properties": {"vejnavn": "Ivar Huitfeldts Gade", "husnnr": "64A", "invalidepladser": 1, "bemrk": null, "ident": "1185", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:51:46.78", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:51:46.78", "mi_style": null, "mi_prinx": 417}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574223.2, 6222506.7]},"properties": {"vejnavn": "Stadion Allé", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:54:59.317", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:54:59.317", "mi_style": null, "mi_prinx": 418}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576141.0, 6225269.5]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 08:59:53.823", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 08:59:53.823", "mi_style": null, "mi_prinx": 419}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576136.6, 6225275.9]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:00:00.397", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:00:00.397", "mi_style": null, "mi_prinx": 420}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576132.525, 6225282.1125]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1186", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:00:19.683", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:01:17.037", "mi_style": null, "mi_prinx": 421}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574134.4, 6222663.3500000015]},"properties": {"vejnavn": "Ålborggade", "husnnr": "16", "invalidepladser": 1, "bemrk": "på NV hjørnet ved Chr. Wærums Gade", "ident": "1187", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:08:44.067", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:08:44.067", "mi_style": null, "mi_prinx": 422}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573822.0, 6222563.450000001]},"properties": {"vejnavn": "P. P. Ørums Gade", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": "1187", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:12:21.367", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:12:21.367", "mi_style": null, "mi_prinx": 423}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570801.2, 6216735.900000001]},"properties": {"vejnavn": "Kirketorvet", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": "1190", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:14:51.303", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:14:51.303", "mi_style": null, "mi_prinx": 424}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570806.0, 6216734.500000001]},"properties": {"vejnavn": "Kirketorvet", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": "1190", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:15:00.997", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:15:00.997", "mi_style": null, "mi_prinx": 425}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574101.2, 6223033.899999999]},"properties": {"vejnavn": "Schleppegrellsgade", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:19:45.767", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:19:45.767", "mi_style": null, "mi_prinx": 426}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574902.6, 6222755.500000001]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:22:33.383", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:22:33.383", "mi_style": null, "mi_prinx": 427}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573502.0, 6224196.900000001]},"properties": {"vejnavn": "Regensburgsgade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1191", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:24:22.523", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:25:10.057", "mi_style": null, "mi_prinx": 428}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574308.35, 6223515.425000001]},"properties": {"vejnavn": "Christen Købkes Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:27:59.887", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:27:59.887", "mi_style": null, "mi_prinx": 430}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575200.2, 6224522.150000001]},"properties": {"vejnavn": "Studsgade", "husnnr": "22", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:29:06.1", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:29:06.1", "mi_style": null, "mi_prinx": 431}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574790.4, 6222623.850000001]},"properties": {"vejnavn": "Assensgade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": "1193", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:31:34.91", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:31:34.91", "mi_style": null, "mi_prinx": 432}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575263.2000000001, 6228819.3]},"properties": {"vejnavn": "Tværmarksvej", "husnnr": "91", "invalidepladser": 1, "bemrk": null, "ident": "1200", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:36:06.61", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:36:06.61", "mi_style": null, "mi_prinx": 433}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574008.1, 6225215.550000001]},"properties": {"vejnavn": "Chr. Molbechs Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1202", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 09:42:59.763", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 09:42:59.763", "mi_style": null, "mi_prinx": 434}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575616.4500000001, 6226158.575000001]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "218", "invalidepladser": 1, "bemrk": "", "ident": "1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:22:30.467", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:22:30.467", "mi_style": null, "mi_prinx": 435}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574331.2000000001, 6227559.55]},"properties": {"vejnavn": "Vorregårds Allé", "husnnr": "125", "invalidepladser": 1, "bemrk": null, "ident": "1205", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:27:08.61", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:27:08.61", "mi_style": null, "mi_prinx": 436}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574076.6, 6222877.450000001]},"properties": {"vejnavn": "Vejlegade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1207", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:35:54.443", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:35:54.443", "mi_style": null, "mi_prinx": 437}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574813.4, 6227367.300000002]},"properties": {"vejnavn": "Solhvervsvej", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:36:25.903", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:58:57.687", "mi_style": null, "mi_prinx": 438}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573701.05, 6225537.125000001]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "37", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:39:11.127", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:39:11.127", "mi_style": null, "mi_prinx": 439}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570415.7999999999, 6224631.05]},"properties": {"vejnavn": "Karen Blixens Boulevard", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": "1208", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:42:06.563", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:42:06.563", "mi_style": null, "mi_prinx": 440}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573264.6000000001, 6225891.1000000015]},"properties": {"vejnavn": "Majdalen", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": "1209", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:46:08.143", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 07:00:59.987", "mi_style": null, "mi_prinx": 441}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575311.4, 6225676.650000001]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": "1213", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 10:52:54.42", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 10:52:54.42", "mi_style": null, "mi_prinx": 442}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575013.2, 6224297.75]},"properties": {"vejnavn": "Badstuegade", "husnnr": "1B", "invalidepladser": 1, "bemrk": null, "ident": "1213", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:06:29.84", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:06:29.84", "mi_style": null, "mi_prinx": 443}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574438.2, 6224426.775000001]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "8", "invalidepladser": 1, "bemrk": null, "ident": "1216", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:12:55.713", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:12:55.713", "mi_style": null, "mi_prinx": 444}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574116.1, 6222681.774999999]},"properties": {"vejnavn": "Chr. Wærums Gade", "husnnr": "11A", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:15:24.13", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:15:24.13", "mi_style": null, "mi_prinx": 445}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575674.7999999999, 6225450.7]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44B", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:25:59.593", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:26:38.423", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 446}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575677.0, 6225459.9]},"properties": {"vejnavn": "Skovvejen", "husnnr": "44A", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:27:24.777", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:27:24.777", "mi_style": "Symbol (34, 16720127, 14)", "mi_prinx": 447}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575345.5377970586, 6224349.986968176]},"properties": {"vejnavn": "Skt. Olufs Gade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": "1217", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:32:05.217", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:59:53.663", "mi_style": null, "mi_prinx": 448}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575681.3, 6226046.85]},"properties": {"vejnavn": "Peder Skrams Gade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "1195+1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:44:10.597", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:44:10.597", "mi_style": null, "mi_prinx": 449}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575668.2000000001, 6226049.55]},"properties": {"vejnavn": "Peder Skrams Gade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": "1195+1203", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:44:22.327", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:44:22.327", "mi_style": null, "mi_prinx": 450}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224469.1]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "29", "invalidepladser": null, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:47:52.29", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:58.087", "mi_style": null, "mi_prinx": 451}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.6000000001, 6224461.9]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "29", "invalidepladser": 1, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:48:07.09", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:49:44.383", "mi_style": null, "mi_prinx": 452}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573564.4, 6224492.5]},"properties": {"vejnavn": "Tage-Hansens Gade", "husnnr": "31B", "invalidepladser": 1, "bemrk": null, "ident": "1196+1212", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:48:39.0", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:48:39.0", "mi_style": null, "mi_prinx": 453}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574462.6000000001, 6222419.500000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:06.373", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:06.373", "mi_style": null, "mi_prinx": 454}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574468.6000000001, 6222420.700000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:12.95", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:12.95", "mi_style": null, "mi_prinx": 455}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574475.0000000001, 6222421.900000001]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:20.613", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:20.613", "mi_style": null, "mi_prinx": 456}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574481.8, 6222423.1000000015]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "34", "invalidepladser": 1, "bemrk": null, "ident": "1218", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-17 11:52:32.217", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-17 11:52:32.217", "mi_style": null, "mi_prinx": 457}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576152.8, 6225252.3]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:51:19.7", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:51:19.7", "mi_style": null, "mi_prinx": 458}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576138.6000000001, 6225293.1]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:51:52.03", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:51:52.03", "mi_style": null, "mi_prinx": 459}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576141.8, 6225288.5]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "10", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:52:02.153", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:52:02.153", "mi_style": null, "mi_prinx": 460}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576156.4, 6225267.7]},"properties": {"vejnavn": "Esther Aggebos Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": "1186+1172", "oprettet_af": "spatial_reader", "oprettet_dato": "2020-03-23 06:52:20.473", "rettet_af": "spatial_reader", "rettet_dato": "2020-03-23 06:52:20.473", "mi_style": null, "mi_prinx": 461}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574173.0585387659, 6222819.027722998]},"properties": {"vejnavn": "Ingerslev Boulevard", "husnnr": null, "invalidepladser": 1, "bemrk": "1200", "ident": "1", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 13:11:36.553", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 13:11:36.553", "mi_style": null, "mi_prinx": 464}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574717.2467898262, 6222608.072481987]},"properties": {"vejnavn": "Assensgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:16:10.33", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:16:10.33", "mi_style": null, "mi_prinx": 466}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574756.4414590374, 6222616.351223999]},"properties": {"vejnavn": "Assensgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:17:39.227", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:17:39.227", "mi_style": null, "mi_prinx": 467}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575071.0398722045, 6223736.640195375]},"properties": {"vejnavn": "Fredensgade", "husnnr": "36", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:23:36.633", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:23:36.633", "mi_style": null, "mi_prinx": 468}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575071.6634185021, 6223738.822607417]},"properties": {"vejnavn": "Fredensgade ", "husnnr": "36", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 14:24:07.44", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 14:24:07.44", "mi_style": null, "mi_prinx": 469}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574458.5545235131, 6224405.154470327]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:01:44.29", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:01:44.29", "mi_style": null, "mi_prinx": 470}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574456.8303596153, 6224406.480750249]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:02:10.15", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:02:10.15", "mi_style": null, "mi_prinx": 471}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574439.588720638, 6224425.8444371]},"properties": {"vejnavn": "Langelandsgade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:02:36.877", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:02:36.877", "mi_style": null, "mi_prinx": 472}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574213.7462111231, 6224317.430271916]},"properties": {"vejnavn": "Mønsgade", "husnnr": "8a", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:06:38.743", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:06:38.743", "mi_style": null, "mi_prinx": 473}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574364.2445136809, 6225576.13351622]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:14:03.827", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:14:03.827", "mi_style": null, "mi_prinx": 474}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574271.226033188, 6225520.486698972]},"properties": {"vejnavn": "Gustav Wieds Vej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:14:25.237", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:14:54.583", "mi_style": null, "mi_prinx": 475}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573648.4455852153, 6222703.284394252]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "9", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:18:50.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:10.813", "mi_style": null, "mi_prinx": 476}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573645.981519507, 6222704.927104725]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "9", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:19:02.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:24.89", "mi_style": null, "mi_prinx": 477}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574615.0200821363, 6226143.664271046]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:24:58.847", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:25:25.463", "mi_style": null, "mi_prinx": 478}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573440.6894959002, 6225916.164055831]},"properties": {"vejnavn": "Fuglesangs Allé ", "husnnr": "115", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:30:49.483", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:31:19.2", "mi_style": null, "mi_prinx": 479}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573441.7161899453, 6225914.829353572]},"properties": {"vejnavn": "Fuglesangs Allé", "husnnr": "115", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:31:13.193", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:31:13.193", "mi_style": null, "mi_prinx": 480}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572528.9192842386, 6223635.9144780645]},"properties": {"vejnavn": "Åbyvej ", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:33:23.21", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:33:23.21", "mi_style": null, "mi_prinx": 481}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [577384.3500328106, 6228392.545887083]},"properties": {"vejnavn": "Vestre Strandallé", "husnnr": "170", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:35:52.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:35:52.45", "mi_style": null, "mi_prinx": 482}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [577384.3500328106, 6228395.831308028]},"properties": {"vejnavn": "Vestre Strandallé", "husnnr": "170", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-10 15:36:07.74", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-10 15:36:07.74", "mi_style": null, "mi_prinx": 483}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574290.6883509178, 6223440.857930892]},"properties": {"vejnavn": "Christen Købkes Gade", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:04:38.56", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:04:38.56", "mi_style": null, "mi_prinx": 485}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574292.8885474477, 6223450.555093376]},"properties": {"vejnavn": "Christen Købkes gade", "husnnr": "25", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": "", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:05:17.92", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:05:17.92", "mi_style": null, "mi_prinx": 486}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574188.2994045179, 6222220.596509239]},"properties": {"vejnavn": "Ellemarksvej", "husnnr": "25", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:09:47.597", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:09:47.597", "mi_style": null, "mi_prinx": 487}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574594.8774293908, 6224862.872887781]},"properties": {"vejnavn": "Falstergade ", "husnnr": "9", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:11:45.25", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:11:45.25", "mi_style": null, "mi_prinx": 488}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575645.3796509246, 6225033.394188903]},"properties": {"vejnavn": "Fiskerivej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:13:23.887", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:13:23.887", "mi_style": null, "mi_prinx": 489}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575687.3336841018, 6225135.283751449]},"properties": {"vejnavn": "Fiskerivej", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:21:28.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:21:28.367", "mi_style": null, "mi_prinx": 490}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573765.5234907598, 6222669.554414783]},"properties": {"vejnavn": "Gråstengade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:23:07.33", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:23:07.33", "mi_style": null, "mi_prinx": 491}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574263.9406273357, 6223167.701885861]},"properties": {"vejnavn": "Holbergsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:37:05.283", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:37:05.283", "mi_style": null, "mi_prinx": 492}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573818.8194244037, 6222564.275600996]},"properties": {"vejnavn": "P.P. Ørums Gade ", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:54:37.353", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:54:37.353", "mi_style": null, "mi_prinx": 494}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575020.143846024, 6224751.283552042]},"properties": {"vejnavn": "Sjællandsgade ", "husnnr": "44", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:55:58.737", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:55:58.737", "mi_style": null, "mi_prinx": 495}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575126.0758521565, 6225602.137577002]},"properties": {"vejnavn": "Skt. Johannes Allé", "husnnr": "4", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": "", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:58:18.097", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:58:18.097", "mi_style": null, "mi_prinx": 497}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575127.7185626287, 6225602.958932239]},"properties": {"vejnavn": "Skt. Johannes Allé ", "husnnr": "4", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 08:58:54.79", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 08:58:54.79", "mi_style": null, "mi_prinx": 498}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574192.3717864477, 6222728.518480494]},"properties": {"vejnavn": "Stadion Allé ", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:01:35.56", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:01:35.56", "mi_style": null, "mi_prinx": 499}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574516.2188090354, 6224495.0]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:04:13.31", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:04:13.31", "mi_style": null, "mi_prinx": 500}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573382.1503490765, 6222436.561601641]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:05:22.27", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:05:22.27", "mi_style": null, "mi_prinx": 501}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574586.7241332495, 6222319.2006106265]},"properties": {"vejnavn": "Marselis tværvej", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:10:21.543", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:10:21.543", "mi_style": null, "mi_prinx": 502}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575512.1720674813, 6225984.295569786]},"properties": {"vejnavn": "Herluf Trolles Gade", "husnnr": "43", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:11:37.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:11:37.07", "mi_style": null, "mi_prinx": 503}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570549.4435318275, 6223329.651950718]},"properties": {"vejnavn": "Louisevej", "husnnr": "60", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:12:39.197", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:12:39.197", "mi_style": null, "mi_prinx": 504}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575432.3019691993, 6213534.279897331]},"properties": {"vejnavn": "Skoleparken", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:15:20.913", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:15:20.913", "mi_style": null, "mi_prinx": 505}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575435.1767125258, 6213534.895913758]},"properties": {"vejnavn": "Skoleparken ", "husnnr": "3", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:15:35.137", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:15:35.137", "mi_style": null, "mi_prinx": 506}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574686.8167566396, 6224112.425570103]},"properties": {"vejnavn": "Blegdammen", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:46:05.277", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:46:05.277", "mi_style": null, "mi_prinx": 507}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574737.8197535932, 6222559.590349074]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:48:59.607", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:48:59.607", "mi_style": null, "mi_prinx": 508}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574724.2673921969, 6222578.276180697]},"properties": {"vejnavn": "Dalgas Avenue", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:49:32.92", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:49:32.92", "mi_style": null, "mi_prinx": 509}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574662.6690560804, 6222698.830589775]},"properties": {"vejnavn": "Fåborggade", "husnnr": "19", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:52:16.67", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:52:16.67", "mi_style": null, "mi_prinx": 510}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573723.7641226394, 6222883.87392884]},"properties": {"vejnavn": "Haderslevgade", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 09:56:02.037", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 09:56:02.037", "mi_style": null, "mi_prinx": 511}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574950.1948158465, 6222518.823101973]},"properties": {"vejnavn": "Kildegården ", "husnnr": "14", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:02:38.327", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:02:38.327", "mi_style": null, "mi_prinx": 512}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573174.3326094088, 6222324.340775997]},"properties": {"vejnavn": "Kirkedammen", "husnnr": "31", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:03:44.91", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:03:44.91", "mi_style": null, "mi_prinx": 513}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574764.3736858368, 6224401.6597698275]},"properties": {"vejnavn": "Klostergade", "husnnr": "68", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:04:40.63", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:04:40.63", "mi_style": null, "mi_prinx": 514}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575484.2643121149, 6224755.312114988]},"properties": {"vejnavn": "Kystvejen", "husnnr": "63", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:05:45.95", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:05:45.95", "mi_style": null, "mi_prinx": 516}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575260.0545790552, 6225740.691868583]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "64", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:06:42.113", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:06:42.113", "mi_style": null, "mi_prinx": 517}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574122.1695396319, 6222253.575999677]},"properties": {"vejnavn": "Ordrupvej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:07:29.987", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:07:29.987", "mi_style": null, "mi_prinx": 518}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573820.2902647848, 6222564.066232556]},"properties": {"vejnavn": "P.P. Ørums Gade", "husnnr": "40", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:08:36.257", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:08:36.257", "mi_style": null, "mi_prinx": 519}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573513.2284804934, 6224421.503059546]},"properties": {"vejnavn": "Thomas Nielsens Gade ", "husnnr": "4b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:35:15.567", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:35:15.567", "mi_style": null, "mi_prinx": 521}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574869.9698357292, 6224686.401437372]},"properties": {"vejnavn": "Thunøgade", "husnnr": "17", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:36:20.32", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:36:20.32", "mi_style": null, "mi_prinx": 522}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574842.0437577005, 6224677.982546201]},"properties": {"vejnavn": "Thunøgade", "husnnr": "21", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:36:47.17", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:36:47.17", "mi_style": null, "mi_prinx": 523}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573819.4550102665, 6222787.195071867]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:38:32.23", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:38:32.23", "mi_style": null, "mi_prinx": 524}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573819.8656878845, 6222774.258726898]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "3", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:38:44.137", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:38:44.137", "mi_style": null, "mi_prinx": 525}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573809.5987474328, 6222763.17043121]},"properties": {"vejnavn": "Langenæs Allé", "husnnr": "5", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:39:04.733", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:39:04.733", "mi_style": null, "mi_prinx": 526}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574862.9051540041, 6226224.204312115]},"properties": {"vejnavn": "Finsensgade", "husnnr": "61", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:40:40.31", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:40:40.31", "mi_style": null, "mi_prinx": 527}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575609.3253388093, 6225642.107802872]},"properties": {"vejnavn": "Niels Juels Gade", "husnnr": "14", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:41:40.133", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:41:40.133", "mi_style": null, "mi_prinx": 528}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575621.7098357282, 6226157.82032854]},"properties": {"vejnavn": "Skovvangsvej", "husnnr": "220", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:42:29.26", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:42:29.26", "mi_style": null, "mi_prinx": 529}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574273.5949691997, 6226579.6673511285]},"properties": {"vejnavn": "Frederikshaldsgade ", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:46:09.1", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:46:09.1", "mi_style": null, "mi_prinx": 530}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574272.9789527727, 6226577.408624229]},"properties": {"vejnavn": "Frederikshaldsgade", "husnnr": "15", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:46:27.437", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:46:27.437", "mi_style": null, "mi_prinx": 531}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574550.2745468095, 6226415.597575942]},"properties": {"vejnavn": "Norges Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:48:32.27", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:48:32.27", "mi_style": null, "mi_prinx": 532}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573464.9690871622, 6225551.886526177]},"properties": {"vejnavn": "Fuglesangs Allé", "husnnr": "114", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:49:51.593", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:49:51.593", "mi_style": null, "mi_prinx": 533}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573284.2962628339, 6226064.2587268995]},"properties": {"vejnavn": "Hammershusvej ", "husnnr": "51", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:50:32.377", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:50:32.377", "mi_style": null, "mi_prinx": 534}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574168.5019342923, 6225689.519246406]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "92d", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:52:34.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:52:34.45", "mi_style": null, "mi_prinx": 536}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573183.6454293831, 6219583.845422071]},"properties": {"vejnavn": "M.P. Hansens Vej", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:54:27.033", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:54:27.033", "mi_style": null, "mi_prinx": 537}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573194.0588431241, 6219583.965116482]},"properties": {"vejnavn": "M.P. Hansens Gade", "husnnr": "18", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:54:45.367", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:54:45.367", "mi_style": null, "mi_prinx": 538}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574293.6617659132, 6222509.108829569]},"properties": {"vejnavn": "F. Vestergaards Gade", "husnnr": "32", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:56:36.283", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:56:36.283", "mi_style": null, "mi_prinx": 539}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574290.7870225868, 6222525.535934292]},"properties": {"vejnavn": "F. Vestergaards Gade ", "husnnr": "32", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:56:51.647", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:56:51.647", "mi_style": null, "mi_prinx": 540}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575014.7018685832, 6224472.544147842]},"properties": {"vejnavn": "Guldsmedegade", "husnnr": "26", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 10:58:17.377", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 10:58:17.377", "mi_style": null, "mi_prinx": 541}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574853.3419507187, 6224400.072895276]},"properties": {"vejnavn": "Klostergade ", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:01:19.687", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:46.85", "mi_style": null, "mi_prinx": 542}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574850.2618685832, 6224400.278234085]},"properties": {"vejnavn": "Klostergade", "husnnr": "56", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:01:29.593", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:01:29.593", "mi_style": null, "mi_prinx": 543}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573800.5521852324, 6222947.219776177]},"properties": {"vejnavn": "Læssøesgade", "husnnr": "83", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:15:14.473", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:15:14.473", "mi_style": null, "mi_prinx": 544}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575433.1519507194, 6224837.509240246]},"properties": {"vejnavn": "Molsgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:27:04.7", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:27:04.7", "mi_style": null, "mi_prinx": 545}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574117.4563655023, 6222280.277207391]},"properties": {"vejnavn": "Ordrupvej", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:27:44.68", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:27:44.68", "mi_style": null, "mi_prinx": 547}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573592.798767967, 6222723.202258727]},"properties": {"vejnavn": "Åbenrågade", "husnnr": "17", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:30:01.607", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:30:01.607", "mi_style": null, "mi_prinx": 548}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573852.0671252563, 6224259.847022587]},"properties": {"vejnavn": "Ringkøbingvej ", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:31:29.06", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:31:29.06", "mi_style": null, "mi_prinx": 549}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573851.8617864472, 6224258.409650924]},"properties": {"vejnavn": "Ringkøbingvej", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:31:45.1", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:31:45.1", "mi_style": null, "mi_prinx": 550}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574375.1585652019, 6222033.415963625]},"properties": {"vejnavn": "Skovbrynet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:33:45.17", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:33:45.17", "mi_style": null, "mi_prinx": 551}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574606.2883778228, 6227299.677618067]},"properties": {"vejnavn": "Vorregårds Allé ", "husnnr": "103", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:34:59.943", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:34:59.943", "mi_style": null, "mi_prinx": 552}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575306.5234312108, 6225834.736901434]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "13", "invalidepladser": 1, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:36:31.937", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:36:31.937", "mi_style": null, "mi_prinx": 553}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575306.9341088288, 6225832.478174535]},"properties": {"vejnavn": "Willemoesgade", "husnnr": "13", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:36:49.883", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:36:49.883", "mi_style": null, "mi_prinx": 554}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573236.9384143031, 6220161.223442913]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:38:24.073", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:38:24.073", "mi_style": null, "mi_prinx": 555}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570728.6893018486, 6216766.413757699]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:42:34.943", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:42:34.943", "mi_style": null, "mi_prinx": 556}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570725.4038809041, 6216767.645790554]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:42:46.957", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:42:46.957", "mi_style": null, "mi_prinx": 557}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570683.72010267, 6216786.947638603]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:20.413", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:20.413", "mi_style": null, "mi_prinx": 558}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570684.5414579061, 6216790.438398357]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:30.79", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:30.79", "mi_style": null, "mi_prinx": 559}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570716.1636344974, 6216831.916837782]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:47.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:47.467", "mi_style": null, "mi_prinx": 560}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [570716.5743121156, 6216834.996919917]},"properties": {"vejnavn": "Kirketorvet", "husnnr": null, "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:43:57.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:43:57.08", "mi_style": null, "mi_prinx": 561}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575070.7447433264, 6223252.195071867]},"properties": {"vejnavn": "Jægergårdsgade", "husnnr": "144", "invalidepladser": 1, "bemrk": "tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:45:13.057", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:45:13.057", "mi_style": null, "mi_prinx": 562}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574936.4408418891, 6222987.421971251]},"properties": {"vejnavn": "Marselisborg Allé", "husnnr": "30b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:46:52.93", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:46:52.93", "mi_style": null, "mi_prinx": 563}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573702.7326710331, 6224158.784515839]},"properties": {"vejnavn": "Regenburgs Plads", "husnnr": "50", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:48:19.28", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:48:19.28", "mi_style": null, "mi_prinx": 564}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573619.7098357283, 6224188.62422998]},"properties": {"vejnavn": "Regenburgsgade", "husnnr": "11", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:50:10.157", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:50:10.157", "mi_style": null, "mi_prinx": 565}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573603.5732238201, 6222298.034907595]},"properties": {"vejnavn": "Rudpolh Wulffs Gade", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:51:55.07", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:51:55.07", "mi_style": null, "mi_prinx": 566}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.381326311, 6223149.484703504]},"properties": {"vejnavn": "st st blichers gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:53:08.913", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:02:57.243", "mi_style": null, "mi_prinx": 567}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574499.2238193017, 6224391.241273099]},"properties": {"vejnavn": "Teglværksgade", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 11:58:48.823", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 11:58:48.823", "mi_style": null, "mi_prinx": 568}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575248.5108008216, 6223656.206817246]},"properties": {"vejnavn": "Toldbodgade", "husnnr": "8b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:31:29.01", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:31:29.01", "mi_style": null, "mi_prinx": 569}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575613.2119507189, 6225969.438398356]},"properties": {"vejnavn": "Herluf Trolles gade", "husnnr": "28", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:35:59.73", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:35:59.73", "mi_style": null, "mi_prinx": 571}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575459.9619507184, 6225872.322381929]},"properties": {"vejnavn": "Otte Ruds Gade", "husnnr": "38", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:36:48.51", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:36:48.51", "mi_style": null, "mi_prinx": 572}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572973.5414579054, 6219822.767967144]},"properties": {"vejnavn": "Rundhøj Alle", "husnnr": "129", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:47:28.83", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:47:28.83", "mi_style": null, "mi_prinx": 573}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573964.5795687882, 6225531.675564681]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "84", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:52:36.853", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:52:36.853", "mi_style": null, "mi_prinx": 574}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573962.7315195069, 6225530.443531826]},"properties": {"vejnavn": "Jens Baggesens Vej", "husnnr": "84", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 13:52:50.743", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 13:52:50.743", "mi_style": null, "mi_prinx": 575}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573763.6589431843, 6225400.732252075]},"properties": {"vejnavn": "Peter Fabers Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:02:57.817", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:02:57.817", "mi_style": null, "mi_prinx": 576}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573569.8198767971, 6225558.664271047]},"properties": {"vejnavn": "Peter Fabers Vej", "husnnr": "45", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:03:59.61", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:03:59.61", "mi_style": null, "mi_prinx": 577}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573689.2009378455, 6225304.105604249]},"properties": {"vejnavn": "Viggo Stuckenbergs Vej", "husnnr": "7", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:05:26.077", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:05:26.077", "mi_style": null, "mi_prinx": 578}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571906.5431211507, 6223317.124229979]},"properties": {"vejnavn": "Hnerik Hertz Vej", "husnnr": "54", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:06:25.737", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:06:25.737", "mi_style": null, "mi_prinx": 579}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572263.050246407, 6223168.713552361]},"properties": {"vejnavn": "O.I. offersens Vej", "husnnr": "13", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:07:10.387", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:07:10.387", "mi_style": null, "mi_prinx": 580}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571635.578603885, 6220454.145748499]},"properties": {"vejnavn": "Grøndalsvej ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:06.363", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:06.363", "mi_style": null, "mi_prinx": 581}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571637.4266531663, 6220455.78845897]},"properties": {"vejnavn": "Grøndalsvej ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:17.097", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:17.097", "mi_style": null, "mi_prinx": 582}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571642.9708010103, 6220460.511251578]},"properties": {"vejnavn": "Grøndalsvej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:10:50.553", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:10:50.553", "mi_style": null, "mi_prinx": 583}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571645.8455443367, 6220461.948623242]},"properties": {"vejnavn": "Grøndalsvej", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:11:00.837", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:11:00.837", "mi_style": null, "mi_prinx": 584}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575130.4157366346, 6223963.371970712]},"properties": {"vejnavn": "Fiskergade", "husnnr": "80", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:18:06.18", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:18:06.18", "mi_style": null, "mi_prinx": 585}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575127.8080963028, 6223964.838768398]},"properties": {"vejnavn": "Fiskergade", "husnnr": "80", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:18:18.71", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:18:18.71", "mi_style": null, "mi_prinx": 586}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575089.0245238632, 6223834.6749242265]},"properties": {"vejnavn": "Østergade", "husnnr": "48", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:19:11.287", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:19:11.287", "mi_style": null, "mi_prinx": 587}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574258.2222792607, 6222755.437371662]},"properties": {"vejnavn": "F. Vestergaards gade", "husnnr": "4", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:20:02.303", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:20:02.303", "mi_style": null, "mi_prinx": 588}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574263.5610882957, 6222709.852156057]},"properties": {"vejnavn": "F. Vestergaards Gade", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-17 14:20:27.033", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-17 14:20:27.033", "mi_style": null, "mi_prinx": 589}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573703.2468583164, 6225533.67761807]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej ", "husnnr": "37", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 13:08:01.087", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 13:08:01.087", "mi_style": null, "mi_prinx": 590}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574949.4344056073, 6222520.606665064]},"properties": {"vejnavn": "Adoplh Meyers Vej", "husnnr": "10", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:06:23.613", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:06:23.613", "mi_style": null, "mi_prinx": 591}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573440.4595687881, 6220071.08932238]},"properties": {"vejnavn": "Rundhøj Allé", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:10:59.32", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:10:59.32", "mi_style": null, "mi_prinx": 592}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574108.964579056, 6219523.614989731]},"properties": {"vejnavn": "Niels Bjerres Vej", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 14:54:28.78", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 14:54:28.78", "mi_style": null, "mi_prinx": 593}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574773.7705954819, 6222620.240246407]},"properties": {"vejnavn": "Assensgade", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-02-24 15:09:24.753", "rettet_af": "spatial_reader", "rettet_dato": "2025-02-24 15:09:24.753", "mi_style": null, "mi_prinx": 594}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573563.520197629, 6225599.931539235]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "65", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-03-03 15:31:57.61", "rettet_af": "spatial_reader", "rettet_dato": "2025-03-03 15:31:57.61", "mi_style": null, "mi_prinx": 595}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573565.9366160922, 6225599.931539235]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "65", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-03-03 15:32:11.45", "rettet_af": "spatial_reader", "rettet_dato": "2025-03-03 15:32:11.45", "mi_style": null, "mi_prinx": 596}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573900.4239732762, 6224444.639830079]},"properties": {"vejnavn": "Eugen Warmings Vej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 12:35:23.093", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:35:23.093", "mi_style": null, "mi_prinx": 597}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573895.0856880753, 6224474.736713882]},"properties": {"vejnavn": "Eugen Warmings Vej", "husnnr": "6", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 12:36:37.753", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 12:36:37.753", "mi_style": null, "mi_prinx": 598}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574904.906065573, 6222700.202576111]},"properties": {"vejnavn": "Hendrik Pontoppidans Gade", "husnnr": "24", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-24 13:10:47.673", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-24 13:10:47.673", "mi_style": null, "mi_prinx": 599}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [576675.4374502097, 6227137.355086146]},"properties": {"vejnavn": "stationsgade", "husnnr": "26", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 09:34:00.233", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 09:34:00.233", "mi_style": null, "mi_prinx": 600}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574471.4348262089, 6222940.82387054]},"properties": {"vejnavn": "Skt Anna Gade", "husnnr": null, "invalidepladser": null, "bemrk": "42", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 14:14:27.8", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 14:14:27.8", "mi_style": null, "mi_prinx": 601}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574477.5688115384, 6222914.615024131]},"properties": {"vejnavn": "Skt Anna Gade", "husnnr": "42", "invalidepladser": null, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-28 14:14:41.96", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-28 14:14:41.96", "mi_style": null, "mi_prinx": 602}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575230.8720257445, 6224047.548393872]},"properties": {"vejnavn": "Kannikegade 16", "husnnr": "16", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 09:56:55.567", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 09:56:55.567", "mi_style": null, "mi_prinx": 603}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571983.2489361251, 6223205.13326127]},"properties": {"vejnavn": "Henrik Hertz Vej ", "husnnr": "72", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 10:59:53.147", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 10:59:53.147", "mi_style": null, "mi_prinx": 604}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [571985.1224724249, 6223202.557148858]},"properties": {"vejnavn": "Henrik Hertz Vej", "husnnr": "72", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 11:01:03.507", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 11:01:03.507", "mi_style": null, "mi_prinx": 605}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574835.1320140511, 6222779.284543324]},"properties": {"vejnavn": "Hans Broges Gade", "husnnr": "17", "invalidepladser": 2, "bemrk": "Tidsbegrænset", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 11:15:39.89", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 11:15:39.89", "mi_style": null, "mi_prinx": 606}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575250.902172792, 6224568.6143739]},"properties": {"vejnavn": "Studsgade ", "husnnr": "44", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-07-31 13:48:01.11", "rettet_af": "spatial_reader", "rettet_dato": "2025-07-31 13:48:01.11", "mi_style": null, "mi_prinx": 607}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574412.2255140772, 6224274.489827803]},"properties": {"vejnavn": "Janus La Cours gade", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:40:20.08", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:40:20.08", "mi_style": null, "mi_prinx": 608}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574548.0404080742, 6226415.892238429]},"properties": {"vejnavn": "Norges Allé ", "husnnr": "2", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:43:11.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:43:11.467", "mi_style": null, "mi_prinx": 609}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573738.9391065245, 6225498.3024416845]},"properties": {"vejnavn": "Vilhelm Bergsøes Vej", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:44:27.91", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:44:27.91", "mi_style": null, "mi_prinx": 610}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573740.2607020313, 6225496.834002232]},"properties": {"vejnavn": "Vilhelm Bergsøes vej", "husnnr": "23", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:44:55.213", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:44:55.213", "mi_style": null, "mi_prinx": 611}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572487.2086247086, 6221123.172494171]},"properties": {"vejnavn": "Rørdamsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:47:21.347", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:47:21.347", "mi_style": null, "mi_prinx": 612}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572485.3438228437, 6221125.736596736]},"properties": {"vejnavn": "Rørdamsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:47:40.887", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:47:40.887", "mi_style": null, "mi_prinx": 613}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573450.7706904352, 6221551.299040821]},"properties": {"vejnavn": "H.C. Ørsteds Vej", "husnnr": "3b", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:49:03.467", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:49:03.467", "mi_style": null, "mi_prinx": 614}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574422.5215850822, 6227269.3811188815]},"properties": {"vejnavn": "Skrydstrupvej", "husnnr": "15", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:50:15.83", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:50:15.83", "mi_style": null, "mi_prinx": 615}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574201.3469197123, 6222224.453960875]},"properties": {"vejnavn": "Ellemarksvej ", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": "1338", "oprettet_af": "spatial_reader", "oprettet_dato": "2025-09-11 11:51:52.827", "rettet_af": "spatial_reader", "rettet_dato": "2025-09-11 11:51:52.827", "mi_style": null, "mi_prinx": 616}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574261.9134668851, 6223538.282346269]},"properties": {"vejnavn": "Eckersbergsgade", "husnnr": "30", "invalidepladser": 1, "bemrk": "1345", "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2025-12-02 13:14:47.253", "rettet_af": "spatial_reader", "rettet_dato": "2025-12-02 13:14:47.253", "mi_style": null, "mi_prinx": 617}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572304.1753244308, 6223126.011642109]},"properties": {"vejnavn": "O. I. Offersensvej ", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 12:51:35.733", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 12:51:35.733", "mi_style": null, "mi_prinx": 618}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574370.9164150944, 6222348.226415094]},"properties": {"vejnavn": "Marselis Boulevard", "husnnr": "33", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 14:21:24.417", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 14:21:24.417", "mi_style": null, "mi_prinx": 619}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574625.108490566, 6226140.433962264]},"properties": {"vejnavn": "Brendstrupvej", "husnnr": "3", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-02-24 14:22:33.083", "rettet_af": "spatial_reader", "rettet_dato": "2026-02-24 14:22:33.083", "mi_style": null, "mi_prinx": 620}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575667.7509942062, 6225512.529924879]},"properties": {"vejnavn": "Trøjborgvej", "husnnr": "2b", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-03 15:32:27.96", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-03 15:32:27.96", "mi_style": null, "mi_prinx": 621}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575717.5020655771, 6225812.445111568]},"properties": {"vejnavn": "Otte Ruds Gade", "husnnr": "5", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-10 15:36:13.387", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-10 15:36:13.387", "mi_style": null, "mi_prinx": 622}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574461.6616818061, 6226572.319186298]},"properties": {"vejnavn": "Stockholmsgade", "husnnr": "1", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-10 16:10:04.953", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-10 16:10:04.953", "mi_style": null, "mi_prinx": 623}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [575466.6230287044, 6226492.72476525]},"properties": {"vejnavn": "Skovfaldet", "husnnr": "12", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-03-24 13:49:38.32", "rettet_af": "spatial_reader", "rettet_dato": "2026-03-24 13:49:38.32", "mi_style": null, "mi_prinx": 624}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574060.9656603774, 6222480.358490566]},"properties": {"vejnavn": "Hans Schourups Gade", "husnnr": "6", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-04-14 13:45:00.16", "rettet_af": "spatial_reader", "rettet_dato": "2026-04-14 13:45:00.16", "mi_style": null, "mi_prinx": 626}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574258.8724698942, 6211072.413328694]},"properties": {"vejnavn": "Dampmøllevej", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:50:52.313", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:50:52.313", "mi_style": null, "mi_prinx": 628}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [574259.8084374957, 6211070.354199971]},"properties": {"vejnavn": "Dampmøllevej ", "husnnr": "4", "invalidepladser": 2, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:51:13.877", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:51:13.877", "mi_style": null, "mi_prinx": 629}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [572717.6960377358, 6224879.641509433]},"properties": {"vejnavn": "Præstevamgsvej", "husnnr": "4", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:52:51.823", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:52:51.823", "mi_style": null, "mi_prinx": 630}}, {"type": "Feature","geometry": {"type": "Point","coordinates": [573286.2358490566, 6225834.424528301]},"properties": {"vejnavn": "Majdalen", "husnnr": "1", "invalidepladser": 1, "bemrk": null, "ident": null, "oprettet_af": "spatial_reader", "oprettet_dato": "2026-09-04 14:54:06.53", "rettet_af": "spatial_reader", "rettet_dato": "2026-09-04 14:54:06.53", "mi_style": null, "mi_prinx": 631}}]} \ No newline at end of file From f0a032f9ba3497b5c280fb51ef3f7461e62631a0 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 14 Sep 2026 20:48:05 +0200 Subject: [PATCH 089/101] =?UTF-8?q?Refactored=20test=20source=20stuff=20?= =?UTF-8?q?=E2=80=93=20again!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Testing.md | 15 +- src/Source/Definition.php | 2 +- .../Command/SourceFetchContentCommand.php | 15 +- src/Test/Command/SourceListCommand.php | 32 + src/Test/Command/SourcesImportCommand.php | 18 +- .../MtmSpatialMaps/TestHandicapParking.php | 12 +- src/Test/Source/Osm/TestHandicapParking.php | 11 +- src/Test/Source/TestDefinition.php | 40 ++ .../api/interpreter?osm-handicap-parking | 599 +++++++++++++++++- 9 files changed, 683 insertions(+), 61 deletions(-) create mode 100644 src/Test/Command/SourceListCommand.php create mode 100644 src/Test/Source/TestDefinition.php diff --git a/docs/Testing.md b/docs/Testing.md index ee1adf7..6642cee 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -4,13 +4,17 @@ For local testing and development we use a test controller that's only enabled in the `dev` and `test` environments. -Furthermore, we use static test sources (fetching locally stored data) for testing and development. By convention, -the ID of a test source starts with `test:`, i.e. they can be listed by running +Furthermore, we use static test sources (fetching locally stored data) for testing and development. Test sources are +identified by the `#[TestDefinition]` attribute (rather than `#[Definition]` as real sources). + +Test sources can be listed with the `test:source:list` command: ```shell -docker compose exec phpfpm php bin/console app:source:list | grep 'test:' +docker compose exec phpfpm php bin/console test:source:list ``` +(the `app:source:list` command will list all source; including test sources.) + Example: Import and show data from the test source `test:mtm_spatialmaps-handicap-parking`: ```shell @@ -30,10 +34,11 @@ The data files can be updated by running docker compose exec phpfpm php bin/console test:source:fetch-content ``` -Load *all test sources* with +As shown above, test sources can be imported just like real sources, but for convenience the `test:sources:import` +command can be used to import *all test sources*: ```shell -docker compose exec phpfpm php bin/console test:source:import +docker compose exec phpfpm php bin/console test:sources:import ``` To empty your local broker, e.g. before loading test data, run diff --git a/src/Source/Definition.php b/src/Source/Definition.php index a50e994..1e994ba 100644 --- a/src/Source/Definition.php +++ b/src/Source/Definition.php @@ -46,7 +46,7 @@ public function __construct( public static function of(string $class): self { $reflection = new \ReflectionClass($class); - $attribute = $reflection->getAttributes(Definition::class)[0] + $attribute = $reflection->getAttributes(Definition::class, flags: \ReflectionAttribute::IS_INSTANCEOF)[0] ?? throw new \LogicException(sprintf('Source %s declares no #[%s] attribute.', $class, Definition::class)); return $attribute->newInstance(); diff --git a/src/Test/Command/SourceFetchContentCommand.php b/src/Test/Command/SourceFetchContentCommand.php index 2e823eb..450b924 100644 --- a/src/Test/Command/SourceFetchContentCommand.php +++ b/src/Test/Command/SourceFetchContentCommand.php @@ -2,8 +2,8 @@ namespace App\Test\Command; -use App\Source\SourceInterface; use App\SourceManager; +use App\Test\Source\TestDefinition; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -29,13 +29,16 @@ public function __invoke( OutputInterface $output, Application $application, ): int { - /** @var SourceInterface[] $testSources */ - $testSources = array_filter($manager->getSources(), static fn (SourceInterface $source) => str_starts_with($source->definition->id, 'test:')); - foreach ($testSources as $source) { + foreach ($manager->getSources() as $source) { + $definition = $source->definition; + if (!$definition instanceof TestDefinition) { + continue; + } + try { $io->section($source); - $url = $source->definition->landingPage; - $filename = preg_replace('@^[a-z]+://[^/]+/test/@', '', $source->definition->accessUrl); + $url = $manager->getSource($definition->sourceId)->definition->accessUrl; + $filename = preg_replace('@^[a-z]+://[^/]+/test/@', '', $definition->accessUrl); $filename = __DIR__.'/../../../tests/resources/'.$filename; if ($filesystem->exists($filename)) { diff --git a/src/Test/Command/SourceListCommand.php b/src/Test/Command/SourceListCommand.php new file mode 100644 index 0000000..6a8deb8 --- /dev/null +++ b/src/Test/Command/SourceListCommand.php @@ -0,0 +1,32 @@ +getSources(), static fn (SourceInterface $source) => $source->definition instanceof TestDefinition); + + $io->writeln(sprintf('#sources: %d', \count($sources))); + foreach ($sources as $source) { + $io->writeln((string) $source); + } + + return Command::SUCCESS; + } +} diff --git a/src/Test/Command/SourcesImportCommand.php b/src/Test/Command/SourcesImportCommand.php index 2d2715e..ac19546 100644 --- a/src/Test/Command/SourcesImportCommand.php +++ b/src/Test/Command/SourcesImportCommand.php @@ -2,18 +2,15 @@ namespace App\Test\Command; -use App\Source\SourceInterface; use App\SourceManager; +use App\Test\Source\TestDefinition; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Attribute\When; -use Symfony\Component\Filesystem\Filesystem; -use Symfony\Contracts\HttpClient\HttpClientInterface; #[AsCommand( name: 'test:sources:import', @@ -25,16 +22,15 @@ class SourcesImportCommand public function __invoke( SymfonyStyle $io, SourceManager $manager, - HttpClientInterface $httpClient, - Filesystem $filesystem, OutputInterface $output, Application $application, - #[Option(description: 'If specified, all successfully read test sources will be imported into the broker')] - bool $import = false, ): int { - /** @var SourceInterface[] $testSources */ - $testSources = array_filter($manager->getSources(), static fn (SourceInterface $source) => str_starts_with($source->definition->id, 'test:')); - foreach ($testSources as $source) { + foreach ($manager->getSources() as $source) { + $definition = $source->definition; + if (!$definition instanceof TestDefinition) { + continue; + } + try { $io->section($source); $input = new ArrayInput([ diff --git a/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php index 86dd040..8ee3be8 100644 --- a/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php +++ b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php @@ -7,7 +7,7 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; -use App\Source\Definition; +use App\Test\Source\TestDefinition; use Symfony\Component\DependencyInjection\Attribute\When; /** @@ -15,22 +15,15 @@ */ #[When('dev')] #[When('test')] -#[Definition( +#[TestDefinition( // By convention the ID as a test source must start with `test:` id: 'test:mtm_spatialmaps-handicap-parking', title: 'Test: Handicapparkering, Aarhus Kommune', - description: '', - publisher: '', - contact: '', - // The landingPage URL is used by the `test:source:fetch-content` command to fetch test content. - landingPage: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', accessUrl: 'http://nginx:8080/test/data/webkort.aarhuskommune.dk/spatialmap?mtm_spatialmaps-handicap-parking', mediaType: 'application/geo+json', crs: 'EPSG:25832', model: 'OnStreetParking', contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', - updateFrequency: '', - licence: null, omittedFields: [ 'ident' => 'Single-letter code; its meaning is not documented and not confirmed by the data owner.', 'oprettet_af' => 'Directory username of the municipal employee who created the record.', @@ -39,6 +32,7 @@ 'rettet_dato' => 'Describes the register record.', 'mi_style' => 'MapInfo rendering style, empty throughout the export.', ], + sourceId: 'mtm_spatialmaps-handicap-parking', )] final class TestHandicapParking extends AbstractSource { diff --git a/src/Test/Source/Osm/TestHandicapParking.php b/src/Test/Source/Osm/TestHandicapParking.php index 48a823c..f71d7a3 100644 --- a/src/Test/Source/Osm/TestHandicapParking.php +++ b/src/Test/Source/Osm/TestHandicapParking.php @@ -8,6 +8,7 @@ use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; use App\Source\Definition; +use App\Test\Source\TestDefinition; use Symfony\Component\DependencyInjection\Attribute\When; /** @@ -15,15 +16,10 @@ */ #[When('dev')] #[When('test')] -#[Definition( +#[TestDefinition( // By convention the ID as a test source must start with `test:` id: 'test:osm-handicap-parking', title: 'Test: Handicapparkering (OpenStreetMap), Aarhus Kommune', - description: '', - publisher: '', - contact: '', - // The landingPage URL is used by the `test:source:fetch-content` command to fetch test content. - landingPage: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', // The Overpass QL in the URL: within Aarhus Municipality (OSM // relation 1784663), select every element tagged as a disabled // parking space (parking_space=disabled) or as reserving bays for @@ -33,8 +29,6 @@ crs: 'EPSG:4326', model: 'OnStreetParking', contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', - updateFrequency: '', - licence: null, omittedFields: [ 'amenity' => 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', @@ -50,6 +44,7 @@ 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', ], + sourceId: 'osm-handicap-parking', )] final class TestHandicapParking extends AbstractSource { diff --git a/src/Test/Source/TestDefinition.php b/src/Test/Source/TestDefinition.php new file mode 100644 index 0000000..58ef497 --- /dev/null +++ b/src/Test/Source/TestDefinition.php @@ -0,0 +1,40 @@ + Date: Tue, 15 Sep 2026 09:46:42 +0200 Subject: [PATCH 090/101] Added additionalInformation to NGSI entities --- src/Ngsi/NgsiEntity.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Ngsi/NgsiEntity.php b/src/Ngsi/NgsiEntity.php index 97d781a..45f575c 100644 --- a/src/Ngsi/NgsiEntity.php +++ b/src/Ngsi/NgsiEntity.php @@ -47,6 +47,18 @@ public function setProperty(string $name, mixed $value, ?string $observedAt = nu return $this; } + /** + * Source fields the model has no attribute for, gathered under one term. + * + * @param array $fields + */ + public function additionalInformation(array $fields): self + { + $isPresent = static fn (mixed $value): bool => null !== $value && '' !== $value; + + return $this->setProperty('additionalInformation', array_filter($fields, $isPresent)); + } + /** * @param array{type: string, coordinates: mixed} $geoJson */ From 5393c571a336fb669fac01416365c66c5ba7c808 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 09:46:47 +0200 Subject: [PATCH 091/101] Carry surface and wheelchair as additional information --- src/Source/Osm/HandicapParking.php | 10 +++++++--- tests/Source/Osm/HandicapParkingTest.php | 23 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index d2cfe44..8ccfba0 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -40,8 +40,6 @@ 'disabled' => 'Access restriction on street-side parking; redundant with the category every entity is published with.', 'access' => 'Who may enter; mapping it onto permit attributes needs an interpretation the tag values do not support.', 'fee:conditional' => 'Time-qualified refinement of fee; the category values the plain fee tag maps onto carry no schedule.', - 'surface' => 'Paving material; the model has no counterpart. 6% of records carry it.', - 'wheelchair' => 'Step-free access to the place, not the parking capacity. 3% of records carry it.', 'capacity:charging' => 'Bays with charging points; a different subset than the reserved bays this data set publishes. 2% of records carry it.', 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', @@ -88,7 +86,13 @@ public function createNgsiEntity(array $data): ?NgsiEntity ->setProperty('category', $this->category($tags)) ->setProperty('totalSpotNumber', $this->reservedBays($tags)) ->setProperty('source', $this->definition->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); + ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)) + + // Add custom attributes + ->additionalInformation([ + 'surface' => trim((string) ($tags['surface'] ?? '')), + 'wheelchair' => trim((string) ($tags['wheelchair'] ?? '')), + ]); } /** diff --git a/tests/Source/Osm/HandicapParkingTest.php b/tests/Source/Osm/HandicapParkingTest.php index 02f8d5d..a508678 100644 --- a/tests/Source/Osm/HandicapParkingTest.php +++ b/tests/Source/Osm/HandicapParkingTest.php @@ -178,13 +178,32 @@ public function testItRecordsTheAccessUrlAsTheEntitySource(): void $this->assertSame($this->source->definition->accessUrl, $this->entities[0]['source']['value']); } + public function testItCarriesTagsTheModelCannotHoldAsAdditionalInformation(): void + { + $this->assertSame( + ['type' => 'Property', 'value' => ['surface' => 'paving_stones', 'wheelchair' => 'yes']], + $this->entities[6]['additionalInformation'] + ); + } + + public function testItCarriesOnlyTheTagsARecordActuallyHas(): void + { + $this->assertSame(['surface' => 'asphalt'], $this->entities[4]['additionalInformation']['value']); + } + + public function testItOmitsAdditionalInformationWhenARecordCarriesNoSuchTag(): void + { + $this->assertArrayNotHasKey('additionalInformation', $this->entities[0]); + } + /** * The first five elements are records from the live feed — a facility * node, a named facility, a closed bay way, a bay way and a relation — * kept verbatim except the second way, whose geometry is cut to two * vertices to exercise the open-way path. The rest are constructed for * the untagged capacity default, capacity:disabled=yes, an unrecognised - * fee value and the two guards that discard a record. + * fee value, the tags carried as additional information, and the two + * guards that discard a record. * * @return list> */ @@ -292,6 +311,8 @@ private function elements(): array 'parking_space' => 'disabled', 'description' => 'Ved hovedindgangen', 'fee' => 'donation', + 'surface' => 'paving_stones', + 'wheelchair' => 'yes', ], ], [ From fe18ba4f2010a1d31617217820f9317d74e87509 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 09:46:51 +0200 Subject: [PATCH 092/101] Fixed attribute name in comment --- src/Source/AbstractSource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Source/AbstractSource.php b/src/Source/AbstractSource.php index 168ac5f..a0794a2 100644 --- a/src/Source/AbstractSource.php +++ b/src/Source/AbstractSource.php @@ -8,7 +8,7 @@ abstract class AbstractSource implements SourceInterface { /** - * Read from the #[AsDataSource] attribute on the concrete source. + * Read from the #[Definition] attribute on the concrete source. */ public Definition $definition { /** From b5bb2e80db6239c46882ef5a0bd71e19dbe43e93 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 09:46:51 +0200 Subject: [PATCH 093/101] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb66a86..12551b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-24](https://github.com/itk-dev/enter/pull/24) + Add housing for custom attributes * [PR-19](https://github.com/itk-dev/enter/pull/19) Declare source metadata in an `#[AsDataSource]` attribute on the source class * [PR-12](https://github.com/itk-dev/enter/pull/12) From a111ae9d697d22ded1f5458e3a436ac495041c99 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 14 Sep 2026 08:54:54 +0200 Subject: [PATCH 094/101] Refactored sources and releated services # Conflicts: # CHANGELOG.md # src/Source/Osm/HandicapParking.php --- CHANGELOG.md | 2 + Taskfile.yml | 10 ++-- src/Command/SourceListCommand.php | 8 +++- src/Command/SourceShowCommand.php | 2 +- src/Source/DataType.php | 9 ++++ src/Source/Definition.php | 8 +++- src/Source/MtmSpatialMaps/HandicapParking.php | 13 ++--- src/Source/Osm/HandicapParking.php | 25 +++++++--- src/Source/SourceInterface.php | 5 +- ...rGeoJson.php => AbstracSourceImporter.php} | 48 ++++++++++--------- src/SourceImporter/GetJsonSourceImporter.php | 24 ++++++++++ src/SourceImporter/OverpassSourceImporter.php | 24 ++++++++++ src/SourceManager.php | 39 ++++++--------- ...urceReaderGeoJson.php => SourceReader.php} | 32 ++++++++----- tests/Source/Osm/HandicapParkingTest.php | 6 +-- 15 files changed, 168 insertions(+), 87 deletions(-) create mode 100644 src/Source/DataType.php rename src/SourceImporter/{SourceImporterGeoJson.php => AbstracSourceImporter.php} (67%) create mode 100644 src/SourceImporter/GetJsonSourceImporter.php create mode 100644 src/SourceImporter/OverpassSourceImporter.php rename src/SourceReader/{SourceReaderGeoJson.php => SourceReader.php} (57%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec5b504..c8ef5a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] * [PR-24](https://github.com/itk-dev/enter/pull/24) + Refactored sources and releated services +* [PR-25](https://github.com/itk-dev/enter/pull/25) Add housing for custom attributes * [PR-10](https://github.com/itk-dev/enter/pull/10) Added test data setup diff --git a/Taskfile.yml b/Taskfile.yml index a1b1178..b781381 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -42,9 +42,9 @@ tasks: test: desc: Run all tests cmds: - - task: test:unit - # - task: test:application - # - task: test:integration + # - task: test:unit + - task: test:geo + - task: test:source test:unit: &test_task env: @@ -55,8 +55,8 @@ tasks: vars: TASK_ARGS: bin/phpunit tests/{{.TASK | replace "test:" "" | title }} - test:integration: *test_task - test:application: *test_task + test:geo: *test_task + test:source: *test_task import: desc: 'Import a source into the broker, e.g. `task {{.TASK}} -- mtm_spatialmaps-handicap-parking`' diff --git a/src/Command/SourceListCommand.php b/src/Command/SourceListCommand.php index 3fcfbeb..c535d38 100644 --- a/src/Command/SourceListCommand.php +++ b/src/Command/SourceListCommand.php @@ -18,10 +18,14 @@ public function __invoke( ): int { $sources = $manager->getSources(); - $io->writeln(sprintf('#sources: %d', \count($sources))); + $headers = ['ID', 'Title', 'Data type']; + $rows = []; + $count = 0; foreach ($sources as $source) { - $io->writeln((string) $source); + $definition = $source->definition; + $rows[] = [$definition->id, $definition->title, $definition->dataType->name]; } + $io->table($headers, $rows); return Command::SUCCESS; } diff --git a/src/Command/SourceShowCommand.php b/src/Command/SourceShowCommand.php index 930b974..35ed900 100644 --- a/src/Command/SourceShowCommand.php +++ b/src/Command/SourceShowCommand.php @@ -18,7 +18,7 @@ public function __invoke(SymfonyStyle $io, #[Argument('The source ID')] SourceInterface $source): int { - $io->writeln(Yaml::dump($source->toArray(), PHP_INT_MAX)); + $io->writeln(Yaml::dump($source->toArray(), inline: PHP_INT_MAX, flags: Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); return Command::SUCCESS; } diff --git a/src/Source/DataType.php b/src/Source/DataType.php new file mode 100644 index 0000000..6ea8983 --- /dev/null +++ b/src/Source/DataType.php @@ -0,0 +1,9 @@ + + * } $accessUrl * @param array $omittedFields */ public function __construct( @@ -22,7 +26,8 @@ public function __construct( public string $publisher, public string $contact, public string $landingPage, - public string $accessUrl, + public string|array $accessUrl, + public DataType $dataType, public string $mediaType, public string $crs, public string $model, @@ -65,6 +70,7 @@ public function toArray(): array 'contact' => $this->contact, 'landing_page' => $this->landingPage, 'access_url' => $this->accessUrl, + 'data_type' => $this->dataType, 'media_type' => $this->mediaType, 'crs' => $this->crs, 'model' => $this->model, diff --git a/src/Source/MtmSpatialMaps/HandicapParking.php b/src/Source/MtmSpatialMaps/HandicapParking.php index 5650c99..4727bbe 100644 --- a/src/Source/MtmSpatialMaps/HandicapParking.php +++ b/src/Source/MtmSpatialMaps/HandicapParking.php @@ -7,6 +7,7 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; +use App\Source\DataType; use App\Source\Definition; /** @@ -20,6 +21,7 @@ contact: 'ppg@aarhus.dk', landingPage: 'https://www.opendata.dk/city-of-aarhus/parkering-i-aarhus-kommune', accessUrl: 'https://webkort.aarhuskommune.dk/spatialmap?page=get_geojson_opendata&datasource=invap', + dataType: DataType::GeoJSON, mediaType: 'application/geo+json', crs: 'EPSG:25832', model: 'OnStreetParking', @@ -42,17 +44,10 @@ )] final class HandicapParking extends AbstractSource { - public function __construct( - private readonly Wgs84Transformer $transformer, - ) { - } - /** * Maps one feed record onto an NgsiEntity. - * - * @param array $data GeoJSON Feature */ - public function createNgsiEntity(array $data): ?NgsiEntity + public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?NgsiEntity { $row = $data['properties'] ?? null; $geometry = $data['geometry'] ?? null; @@ -80,7 +75,7 @@ public function createNgsiEntity(array $data): ?NgsiEntity ->setProperty('category', ['forDisabled']) ->setProperty('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) ->setProperty('source', $this->definition->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); + ->geoProperty('location', $transformer->transformGeometry($this->definition->crs, $geometry)); } /** diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index 8ccfba0..889ae0a 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -7,6 +7,7 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; +use App\Source\DataType; use App\Source\Definition; /** @@ -24,7 +25,22 @@ // relation 1784663), select every element tagged as a disabled // parking space (parking_space=disabled) or as reserving bays for // disabled parking (capacity:disabled, excluding "no" and "0"). - accessUrl: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', + // accessUrl: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', + accessUrl: [ + 'url' => 'https://overpass-api.de/api/interpreter', + 'query' => [ + 'data' => <<<'DATA' +[out:json][timeout:180]; +area(3601784663)->.a; +( + nwr["parking_space"="disabled"](area.a); + nwr["capacity:disabled"]["capacity:disabled"!~"^(no|0)$"](area.a); +); +out geom tags; +DATA, + ], + ], + dataType: DataType::Overpass, mediaType: 'application/json', crs: 'EPSG:4326', model: 'OnStreetParking', @@ -47,17 +63,12 @@ )] final class HandicapParking extends AbstractSource { - public function __construct( - private readonly Wgs84Transformer $transformer, - ) { - } - /** * Maps one feed record onto an NgsiEntity. * * @param array $data Overpass JSON element */ - public function createNgsiEntity(array $data): ?NgsiEntity + public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?NgsiEntity { $type = $data['type'] ?? null; $id = $data['id'] ?? null; diff --git a/src/Source/SourceInterface.php b/src/Source/SourceInterface.php index 11f750f..4868d8e 100644 --- a/src/Source/SourceInterface.php +++ b/src/Source/SourceInterface.php @@ -4,6 +4,7 @@ namespace App\Source; +use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; @@ -22,9 +23,11 @@ interface SourceInterface extends \Stringable, \JsonSerializable } /** + * Maps one feed record onto an NgsiEntity. + * * @param array $data */ - public function createNgsiEntity(array $data): ?NgsiEntity; + public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?NgsiEntity; /** * @return array diff --git a/src/SourceImporter/SourceImporterGeoJson.php b/src/SourceImporter/AbstracSourceImporter.php similarity index 67% rename from src/SourceImporter/SourceImporterGeoJson.php rename to src/SourceImporter/AbstracSourceImporter.php index 41a9dcc..183c107 100644 --- a/src/SourceImporter/SourceImporterGeoJson.php +++ b/src/SourceImporter/AbstracSourceImporter.php @@ -3,22 +3,18 @@ namespace App\SourceImporter; use App\Broker\NgsiLdBroker; +use App\Geo\Wgs84Transformer; use App\Import\Exception\UpsertFailedException; use App\Import\ImportResult; use App\Ngsi\NgsiEntity; use App\Source\SourceInterface; -use App\SourceReader\SourceReaderGeoJson; +use App\SourceReader\SourceReaderInterface; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; use Psr\Log\LoggerTrait; use Symfony\Component\DependencyInjection\Attribute\Autowire; -/** - * Read data from a source and upserts in broker. - * - * @todo Get the importer from a factory depending - */ -class SourceImporterGeoJson implements SourceImporterInterface +abstract class AbstracSourceImporter implements SourceImporterInterface { use LoggerAwareTrait; use LoggerTrait; @@ -27,8 +23,9 @@ class SourceImporterGeoJson implements SourceImporterInterface * @param list $contextUrls */ public function __construct( - private readonly SourceReaderGeoJson $reader, + private readonly SourceReaderInterface $reader, private readonly NgsiLdBroker $broker, + private readonly Wgs84Transformer $transformer, #[Autowire(env: 'json:APP_NGSI_CONTEXT_URLS')] private readonly array $contextUrls, LoggerInterface $logger, @@ -36,11 +33,18 @@ public function __construct( $this->setLogger($logger); } - public function supports(SourceInterface $source): bool - { - // @todo - return true; - } + abstract public function supports(SourceInterface $source): bool; + + /** + * Extract items from array data from source. + * + * Each item must be processable by the source. + * + * @param iterable $data + * + * @return array> + */ + abstract protected function extractItems(iterable $data, SourceInterface $source): array; public function import(SourceInterface $source): ImportResult { @@ -74,16 +78,16 @@ public function import(SourceInterface $source): ImportResult private function read(SourceInterface $source): iterable { $data = $this->reader->read($source); - foreach ($data as $item) { - if (is_array($item)) { - try { - if ($entity = $source->createNgsiEntity($item)) { - yield $entity; - } - } catch (\Exception $e) { - // @todo Log in database? - $this->error('error: {message} ', ['message' => $e->getMessage()]); + $items = $this->extractItems($data, $source); + + foreach ($items as $item) { + try { + if ($entity = $source->createNgsiEntity($item, $this->transformer)) { + yield $entity; } + } catch (\Exception $e) { + // @todo Log in database? + $this->error('error: {message} ', ['message' => $e->getMessage()]); } } } diff --git a/src/SourceImporter/GetJsonSourceImporter.php b/src/SourceImporter/GetJsonSourceImporter.php new file mode 100644 index 0000000..f1e8761 --- /dev/null +++ b/src/SourceImporter/GetJsonSourceImporter.php @@ -0,0 +1,24 @@ +definition->dataType; + } + + protected function extractItems(iterable $data, SourceInterface $source): array + { + $items = $data['features'] ?? null; + if (!is_array($items) || !array_is_list($items)) { + throw new \RuntimeException('Invalid or missing features in data'); + } + + return $items; + } +} diff --git a/src/SourceImporter/OverpassSourceImporter.php b/src/SourceImporter/OverpassSourceImporter.php new file mode 100644 index 0000000..9d9a8fb --- /dev/null +++ b/src/SourceImporter/OverpassSourceImporter.php @@ -0,0 +1,24 @@ +definition->dataType; + } + + protected function extractItems(iterable $data, SourceInterface $source): array + { + $items = $data['elements'] ?? null; + if (!is_array($items) || !array_is_list($items)) { + throw new \RuntimeException('Invalid or missing elements in data'); + } + + return $items; + } +} diff --git a/src/SourceManager.php b/src/SourceManager.php index 8df03a1..c2fe159 100644 --- a/src/SourceManager.php +++ b/src/SourceManager.php @@ -2,26 +2,30 @@ namespace App; -use App\Source\AbstractSource; use App\Source\SourceInterface; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; final class SourceManager { - /** - * Sources indexed by ID. - * - * @var array - */ - private array $indexed; - /** * @param iterable $sources */ public function __construct( #[AutowireIterator('app.source')] - private readonly iterable $sources, + private iterable $sources, ) { + $indexed = []; + foreach ($sources as $source) { + if (!$source instanceof SourceInterface) { + throw new \InvalidArgumentException(sprintf('Invalid source class: %s (must implement %s)', $source::class, SourceInterface::class)); + } + $id = $source->definition->id; + if (isset($indexed[$id])) { + throw new \RuntimeException(sprintf('Duplicate source: %s (ID already used by %s)', $id, $indexed[$id]::class)); + } + $indexed[$id] = $source; + } + $this->sources = $indexed; } /** @@ -31,22 +35,7 @@ public function __construct( */ public function getSources(): array { - if (!isset($this->indexed)) { - $sources = []; - foreach ($this->sources as $source) { - if (!$source instanceof SourceInterface) { - throw new \InvalidArgumentException(sprintf('Invalid source class: %s (must extend %s)', $source::class, AbstractSource::class)); - } - $id = $source->definition->id; - if (isset($sources[$id])) { - throw new \RuntimeException(sprintf('Duplicate source: %s (ID already used by %s)', $id, $sources[$id]::class)); - } - $sources[$id] = $source; - } - $this->indexed = $sources; - } - - return $this->indexed; + return $this->sources; } public function getSource(string $name): SourceInterface diff --git a/src/SourceReader/SourceReaderGeoJson.php b/src/SourceReader/SourceReader.php similarity index 57% rename from src/SourceReader/SourceReaderGeoJson.php rename to src/SourceReader/SourceReader.php index f3fb50d..4099f5e 100644 --- a/src/SourceReader/SourceReaderGeoJson.php +++ b/src/SourceReader/SourceReader.php @@ -7,7 +7,7 @@ use Psr\Log\LoggerTrait; use Symfony\Contracts\HttpClient\HttpClientInterface; -class SourceReaderGeoJson implements SourceReaderInterface +final class SourceReader implements SourceReaderInterface { use LoggerAwareTrait; use LoggerTrait; @@ -22,21 +22,31 @@ public function read(SourceInterface $source): iterable // @todo Add some proper exception handling/logging. $data = $this->getData($source->definition->accessUrl); - $features = $data['features']; - - if (!is_array($features) || !array_is_list($features)) { - throw new \RuntimeException('Invalid features array'); - } - - return $features; + return $data; } /** - * @return array + * @param string|array{ + * url: string, + * query: array + * } $url + * + * @return iterable */ - private function getData(string $url): iterable + private function getData(string|array $url): iterable { - return $this->client->request('GET', $url)->toArray(); + // @todo Cache request responses. + $query = []; + if (is_array($url)) { + [ + 'url' => $url, + 'query' => $query, + ] = $url; + } + + return $this->client->request('GET', $url, [ + 'query' => $query, + ])->toArray(); } /** diff --git a/tests/Source/Osm/HandicapParkingTest.php b/tests/Source/Osm/HandicapParkingTest.php index a508678..859fb2b 100644 --- a/tests/Source/Osm/HandicapParkingTest.php +++ b/tests/Source/Osm/HandicapParkingTest.php @@ -22,12 +22,12 @@ class HandicapParkingTest extends TestCase protected function setUp(): void { - $this->source = new HandicapParking(new Wgs84Transformer()); - + $this->source = new HandicapParking(); + $transformer = new Wgs84Transformer(); $this->entities = array_values(array_map( static fn (NgsiEntity $entity): array => $entity->toPayload(['https://example.com/context.jsonld']), array_filter(array_map( - $this->source->createNgsiEntity(...), + fn (array $data) => $this->source->createNgsiEntity($data, $transformer), $this->elements() )) )); From 50d2e23801dda67d73557212cb026aa76541238b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 09:58:30 +0200 Subject: [PATCH 095/101] Coding standards --- src/Test/Source/MtmSpatialMaps/TestHandicapParking.php | 2 +- src/Test/Source/Osm/TestHandicapParking.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php index 8ee3be8..035930b 100644 --- a/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php +++ b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php @@ -46,7 +46,7 @@ public function __construct( * * @param array $data GeoJSON Feature */ - public function createNgsiEntity(array $data): ?NgsiEntity + public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?NgsiEntity { $row = $data['properties'] ?? null; $geometry = $data['geometry'] ?? null; diff --git a/src/Test/Source/Osm/TestHandicapParking.php b/src/Test/Source/Osm/TestHandicapParking.php index f71d7a3..f8554d9 100644 --- a/src/Test/Source/Osm/TestHandicapParking.php +++ b/src/Test/Source/Osm/TestHandicapParking.php @@ -58,7 +58,7 @@ public function __construct( * * @param array $data Overpass JSON element */ - public function createNgsiEntity(array $data): ?NgsiEntity + public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?NgsiEntity { $type = $data['type'] ?? null; $id = $data['id'] ?? null; From 0acee41d3197332836e128549b9bad6252dbb2d2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 10:17:10 +0200 Subject: [PATCH 096/101] Aligned test setup with the new importer approach The test sources were written against the SourceInterface and Definition as they were before the source refactoring, so they no longer matched: * Pass the Wgs84Transformer to createNgsiEntity instead of injecting it into the constructor, as the real sources now do. * Forward the new dataType through TestDefinition, and declare it on each test source so the right importer unwraps the feed. * Read the url and query of an accessUrl through Definition rather than destructuring the array in every caller, so a source declaring a query can be fetched as well as read. Refreshed the OpenStreetMap test fixture while we were at it. --- src/Source/Definition.php | 22 +++++++++++++++ src/SourceReader/SourceReader.php | 27 +++---------------- .../Command/SourceFetchContentCommand.php | 8 ++++-- .../MtmSpatialMaps/TestHandicapParking.php | 9 +++---- src/Test/Source/Osm/TestHandicapParking.php | 10 +++---- src/Test/Source/TestDefinition.php | 3 +++ .../api/interpreter?osm-handicap-parking | 15 +++++++++-- 7 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/Source/Definition.php b/src/Source/Definition.php index f5e2fcc..42a785f 100644 --- a/src/Source/Definition.php +++ b/src/Source/Definition.php @@ -57,6 +57,28 @@ public static function of(string $class): self return $attribute->newInstance(); } + /** + * The URL to request, without the query parameters. + * + * A source that has to spell out a long query — an Overpass QL script, + * say — declares it as a separate array rather than percent-encoding it + * into the URL by hand. + */ + public function accessUrlBase(): string + { + return is_array($this->accessUrl) ? $this->accessUrl['url'] : $this->accessUrl; + } + + /** + * The query parameters to send with the request. + * + * @return array + */ + public function accessUrlQuery(): array + { + return is_array($this->accessUrl) ? $this->accessUrl['query'] : []; + } + /** * @return array */ diff --git a/src/SourceReader/SourceReader.php b/src/SourceReader/SourceReader.php index 4099f5e..2c90212 100644 --- a/src/SourceReader/SourceReader.php +++ b/src/SourceReader/SourceReader.php @@ -20,32 +20,11 @@ public function __construct( public function read(SourceInterface $source): iterable { // @todo Add some proper exception handling/logging. - $data = $this->getData($source->definition->accessUrl); - - return $data; - } - - /** - * @param string|array{ - * url: string, - * query: array - * } $url - * - * @return iterable - */ - private function getData(string|array $url): iterable - { // @todo Cache request responses. - $query = []; - if (is_array($url)) { - [ - 'url' => $url, - 'query' => $query, - ] = $url; - } + $definition = $source->definition; - return $this->client->request('GET', $url, [ - 'query' => $query, + return $this->client->request('GET', $definition->accessUrlBase(), [ + 'query' => $definition->accessUrlQuery(), ])->toArray(); } diff --git a/src/Test/Command/SourceFetchContentCommand.php b/src/Test/Command/SourceFetchContentCommand.php index 450b924..97753b3 100644 --- a/src/Test/Command/SourceFetchContentCommand.php +++ b/src/Test/Command/SourceFetchContentCommand.php @@ -37,7 +37,9 @@ public function __invoke( try { $io->section($source); - $url = $manager->getSource($definition->sourceId)->definition->accessUrl; + $sourceDefinition = $manager->getSource($definition->sourceId)->definition; + $url = $sourceDefinition->accessUrlBase(); + $query = $sourceDefinition->accessUrlQuery(); $filename = preg_replace('@^[a-z]+://[^/]+/test/@', '', $definition->accessUrl); $filename = __DIR__.'/../../../tests/resources/'.$filename; @@ -46,7 +48,9 @@ public function __invoke( } $io->writeln(sprintf('Fetching "%s"', $url)); - $response = $httpClient->request(Request::METHOD_GET, $url); + $response = $httpClient->request(Request::METHOD_GET, $url, [ + 'query' => $query, + ]); $content = $response->getContent(); $filesystem->dumpFile($filename, $content); $io->success(sprintf('Content written to file %s', realpath($filename))); diff --git a/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php index 035930b..f71ca65 100644 --- a/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php +++ b/src/Test/Source/MtmSpatialMaps/TestHandicapParking.php @@ -7,6 +7,7 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; +use App\Source\DataType; use App\Test\Source\TestDefinition; use Symfony\Component\DependencyInjection\Attribute\When; @@ -20,6 +21,7 @@ id: 'test:mtm_spatialmaps-handicap-parking', title: 'Test: Handicapparkering, Aarhus Kommune', accessUrl: 'http://nginx:8080/test/data/webkort.aarhuskommune.dk/spatialmap?mtm_spatialmaps-handicap-parking', + dataType: DataType::GeoJSON, mediaType: 'application/geo+json', crs: 'EPSG:25832', model: 'OnStreetParking', @@ -36,11 +38,6 @@ )] final class TestHandicapParking extends AbstractSource { - public function __construct( - private readonly Wgs84Transformer $transformer, - ) { - } - /** * Maps one feed record onto an NgsiEntity. * @@ -74,7 +71,7 @@ public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?N ->setProperty('category', ['forDisabled']) ->setProperty('totalSpotNumber', (int) ($row['invalidepladser'] ?? 0)) ->setProperty('source', $this->definition->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); + ->geoProperty('location', $transformer->transformGeometry($this->definition->crs, $geometry)); } /** diff --git a/src/Test/Source/Osm/TestHandicapParking.php b/src/Test/Source/Osm/TestHandicapParking.php index f8554d9..4c3c8b2 100644 --- a/src/Test/Source/Osm/TestHandicapParking.php +++ b/src/Test/Source/Osm/TestHandicapParking.php @@ -7,7 +7,7 @@ use App\Geo\Wgs84Transformer; use App\Ngsi\NgsiEntity; use App\Source\AbstractSource; -use App\Source\Definition; +use App\Source\DataType; use App\Test\Source\TestDefinition; use Symfony\Component\DependencyInjection\Attribute\When; @@ -25,6 +25,7 @@ // parking space (parking_space=disabled) or as reserving bays for // disabled parking (capacity:disabled, excluding "no" and "0"). accessUrl: 'http://nginx:8080/test/data/overpass-api.de/api/interpreter?osm-handicap-parking', + dataType: DataType::Overpass, mediaType: 'application/json', crs: 'EPSG:4326', model: 'OnStreetParking', @@ -48,11 +49,6 @@ )] final class TestHandicapParking extends AbstractSource { - public function __construct( - private readonly Wgs84Transformer $transformer, - ) { - } - /** * Maps one feed record onto an NgsiEntity. * @@ -87,7 +83,7 @@ public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?N ->setProperty('category', $this->category($tags)) ->setProperty('totalSpotNumber', $this->reservedBays($tags)) ->setProperty('source', $this->definition->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)); + ->geoProperty('location', $transformer->transformGeometry($this->definition->crs, $geometry)); } /** diff --git a/src/Test/Source/TestDefinition.php b/src/Test/Source/TestDefinition.php index 58ef497..eb14427 100644 --- a/src/Test/Source/TestDefinition.php +++ b/src/Test/Source/TestDefinition.php @@ -4,6 +4,7 @@ namespace App\Test\Source; +use App\Source\DataType; use App\Source\Definition; #[\Attribute(\Attribute::TARGET_CLASS)] @@ -13,6 +14,7 @@ public function __construct( string $id, string $title, string $accessUrl, + DataType $dataType, string $mediaType, string $crs, string $model, @@ -28,6 +30,7 @@ public function __construct( contact: '', landingPage: '', accessUrl: $accessUrl, + dataType: $dataType, mediaType: $mediaType, crs: $crs, model: $model, diff --git a/tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking b/tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking index 80d73fd..93495a2 100644 --- a/tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking +++ b/tests/resources/data/overpass-api.de/api/interpreter?osm-handicap-parking @@ -2,8 +2,8 @@ "version": 0.6, "generator": "Overpass API 0.7.62.11 87bfad18", "osm3s": { - "timestamp_osm_base": "2026-09-14T18:28:38Z", - "timestamp_areas_base": "2026-09-13T09:23:35Z", + "timestamp_osm_base": "2026-09-15T08:07:27Z", + "timestamp_areas_base": "2026-09-14T02:10:25Z", "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL." }, "elements": [ @@ -2260,6 +2260,17 @@ "parking_space": "disabled" } }, +{ + "type": "node", + "id": 14182120499, + "lat": 56.2677701, + "lon": 10.3121203, + "tags": { + "amenity": "parking_space", + "capacity": "1", + "parking_space": "disabled" + } +}, { "type": "way", "id": 23858719, From fdea0eeb58419ea7102537f3d4c90f1ad51d6f30 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 14:12:52 +0200 Subject: [PATCH 097/101] Declared the access URL query as its own field The access URL was a string|array union: a plain URL, or a map of url and query for the one source that needs a long Overpass QL script. Two reader methods hid the union from callers, but the raw property stayed public and two of its users never learned to disambiguate it. So the OSM source published its whole Overpass script as the NGSI source property of every entity, and as access_url in the catalogue, where a URL belongs. The mapping test asserted the property against the definition it came from, so it compared the array with itself and passed. Splitting the query into its own field settles it: the access URL is a string again and cannot be anything else, accessUrlBase() and accessUrlQuery() are gone, and their two callers read the properties. Sources that send no query say nothing, as before. --- src/Source/Definition.php | 31 +++---------------- src/Source/Osm/HandicapParking.php | 28 ++++++++--------- src/SourceReader/SourceReader.php | 4 +-- .../Command/SourceFetchContentCommand.php | 4 +-- 4 files changed, 21 insertions(+), 46 deletions(-) diff --git a/src/Source/Definition.php b/src/Source/Definition.php index 42a785f..a5e1e4c 100644 --- a/src/Source/Definition.php +++ b/src/Source/Definition.php @@ -13,11 +13,8 @@ readonly class Definition { /** - * @param string|array{ - * url: string, - * query: array - * } $accessUrl * @param array $omittedFields + * @param array $accessUrlQuery */ public function __construct( public string $id, @@ -26,7 +23,7 @@ public function __construct( public string $publisher, public string $contact, public string $landingPage, - public string|array $accessUrl, + public string $accessUrl, public DataType $dataType, public string $mediaType, public string $crs, @@ -35,6 +32,7 @@ public function __construct( public string $updateFrequency, public ?string $licence, public array $omittedFields, + public array $accessUrlQuery = [], ) { } @@ -57,28 +55,6 @@ public static function of(string $class): self return $attribute->newInstance(); } - /** - * The URL to request, without the query parameters. - * - * A source that has to spell out a long query — an Overpass QL script, - * say — declares it as a separate array rather than percent-encoding it - * into the URL by hand. - */ - public function accessUrlBase(): string - { - return is_array($this->accessUrl) ? $this->accessUrl['url'] : $this->accessUrl; - } - - /** - * The query parameters to send with the request. - * - * @return array - */ - public function accessUrlQuery(): array - { - return is_array($this->accessUrl) ? $this->accessUrl['query'] : []; - } - /** * @return array */ @@ -92,6 +68,7 @@ public function toArray(): array 'contact' => $this->contact, 'landing_page' => $this->landingPage, 'access_url' => $this->accessUrl, + 'access_url_query' => $this->accessUrlQuery, 'data_type' => $this->dataType, 'media_type' => $this->mediaType, 'crs' => $this->crs, diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index 889ae0a..efa283a 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -26,20 +26,7 @@ // parking space (parking_space=disabled) or as reserving bays for // disabled parking (capacity:disabled, excluding "no" and "0"). // accessUrl: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', - accessUrl: [ - 'url' => 'https://overpass-api.de/api/interpreter', - 'query' => [ - 'data' => <<<'DATA' -[out:json][timeout:180]; -area(3601784663)->.a; -( - nwr["parking_space"="disabled"](area.a); - nwr["capacity:disabled"]["capacity:disabled"!~"^(no|0)$"](area.a); -); -out geom tags; -DATA, - ], - ], + accessUrl: 'https://overpass-api.de/api/interpreter', dataType: DataType::Overpass, mediaType: 'application/json', crs: 'EPSG:4326', @@ -47,7 +34,6 @@ contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', updateFrequency: 'continuous', licence: 'https://opendatacommons.org/licenses/odbl/1-0/', - omittedFields: [ 'amenity' => 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', 'capacity' => 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.', @@ -60,6 +46,18 @@ 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', ], + + accessUrlQuery: [ + 'data' => <<<'DATA' + [out:json][timeout:180]; + area(3601784663)->.a; + ( + nwr["parking_space"="disabled"](area.a); + nwr["capacity:disabled"]["capacity:disabled"!~"^(no|0)$"](area.a); + ); + out geom tags; + DATA, + ], )] final class HandicapParking extends AbstractSource { diff --git a/src/SourceReader/SourceReader.php b/src/SourceReader/SourceReader.php index 2c90212..ecfa362 100644 --- a/src/SourceReader/SourceReader.php +++ b/src/SourceReader/SourceReader.php @@ -23,8 +23,8 @@ public function read(SourceInterface $source): iterable // @todo Cache request responses. $definition = $source->definition; - return $this->client->request('GET', $definition->accessUrlBase(), [ - 'query' => $definition->accessUrlQuery(), + return $this->client->request('GET', $definition->accessUrl, [ + 'query' => $definition->accessUrlQuery, ])->toArray(); } diff --git a/src/Test/Command/SourceFetchContentCommand.php b/src/Test/Command/SourceFetchContentCommand.php index 97753b3..6edaa73 100644 --- a/src/Test/Command/SourceFetchContentCommand.php +++ b/src/Test/Command/SourceFetchContentCommand.php @@ -38,8 +38,8 @@ public function __invoke( try { $io->section($source); $sourceDefinition = $manager->getSource($definition->sourceId)->definition; - $url = $sourceDefinition->accessUrlBase(); - $query = $sourceDefinition->accessUrlQuery(); + $url = $sourceDefinition->accessUrl; + $query = $sourceDefinition->accessUrlQuery; $filename = preg_replace('@^[a-z]+://[^/]+/test/@', '', $definition->accessUrl); $filename = __DIR__.'/../../../tests/resources/'.$filename; From aa06d3a81469f43e53c5abf07293f12ec19f43f5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 14:24:41 +0200 Subject: [PATCH 098/101] Revert "Declared the access URL query as its own field" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 14b4dad2a739b556431b442af26e8c03a3d11bae. The string|array access URL is the shape feature/source-stuff settled on, so reshaping it belongs with that work rather than here. This branch stays what its name says: the test setup aligned with the importer approach. accessUrlBase() and accessUrlQuery() stay. They are part of that alignment — they moved the destructuring out of SourceReader::getData so the fetch command can reach a source's url and query too. --- src/Source/Definition.php | 31 ++++++++++++++++--- src/Source/Osm/HandicapParking.php | 28 +++++++++-------- src/SourceReader/SourceReader.php | 4 +-- .../Command/SourceFetchContentCommand.php | 4 +-- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/Source/Definition.php b/src/Source/Definition.php index a5e1e4c..42a785f 100644 --- a/src/Source/Definition.php +++ b/src/Source/Definition.php @@ -13,8 +13,11 @@ readonly class Definition { /** + * @param string|array{ + * url: string, + * query: array + * } $accessUrl * @param array $omittedFields - * @param array $accessUrlQuery */ public function __construct( public string $id, @@ -23,7 +26,7 @@ public function __construct( public string $publisher, public string $contact, public string $landingPage, - public string $accessUrl, + public string|array $accessUrl, public DataType $dataType, public string $mediaType, public string $crs, @@ -32,7 +35,6 @@ public function __construct( public string $updateFrequency, public ?string $licence, public array $omittedFields, - public array $accessUrlQuery = [], ) { } @@ -55,6 +57,28 @@ public static function of(string $class): self return $attribute->newInstance(); } + /** + * The URL to request, without the query parameters. + * + * A source that has to spell out a long query — an Overpass QL script, + * say — declares it as a separate array rather than percent-encoding it + * into the URL by hand. + */ + public function accessUrlBase(): string + { + return is_array($this->accessUrl) ? $this->accessUrl['url'] : $this->accessUrl; + } + + /** + * The query parameters to send with the request. + * + * @return array + */ + public function accessUrlQuery(): array + { + return is_array($this->accessUrl) ? $this->accessUrl['query'] : []; + } + /** * @return array */ @@ -68,7 +92,6 @@ public function toArray(): array 'contact' => $this->contact, 'landing_page' => $this->landingPage, 'access_url' => $this->accessUrl, - 'access_url_query' => $this->accessUrlQuery, 'data_type' => $this->dataType, 'media_type' => $this->mediaType, 'crs' => $this->crs, diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index efa283a..889ae0a 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -26,7 +26,20 @@ // parking space (parking_space=disabled) or as reserving bays for // disabled parking (capacity:disabled, excluding "no" and "0"). // accessUrl: 'https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A180%5D%3Barea%283601784663%29-%3E.a%3B%28nwr%5B%22parking_space%22%3D%22disabled%22%5D%28area.a%29%3Bnwr%5B%22capacity%3Adisabled%22%5D%5B%22capacity%3Adisabled%22%21~%22%5E%28no%7C0%29%24%22%5D%28area.a%29%3B%29%3Bout%20geom%20tags%3B', - accessUrl: 'https://overpass-api.de/api/interpreter', + accessUrl: [ + 'url' => 'https://overpass-api.de/api/interpreter', + 'query' => [ + 'data' => <<<'DATA' +[out:json][timeout:180]; +area(3601784663)->.a; +( + nwr["parking_space"="disabled"](area.a); + nwr["capacity:disabled"]["capacity:disabled"!~"^(no|0)$"](area.a); +); +out geom tags; +DATA, + ], + ], dataType: DataType::Overpass, mediaType: 'application/json', crs: 'EPSG:4326', @@ -34,6 +47,7 @@ contextUrl: 'https://raw.githubusercontent.com/smart-data-models/dataModel.Parking/master/context.jsonld', updateFrequency: 'continuous', licence: 'https://opendatacommons.org/licenses/odbl/1-0/', + omittedFields: [ 'amenity' => 'Selector distinguishing a single bay (parking_space) from a facility (parking); the model carries no such distinction.', 'capacity' => 'Published for single bays only; on a facility it counts all bays and would overstate the reserved capacity.', @@ -46,18 +60,6 @@ 'operator' => 'Who runs the facility; a fact about the business rather than its reserved bays. 1% of records carry it.', 'brand' => 'Commercial brand of the facility; the name already identifies it. Under 1% of records carry it.', ], - - accessUrlQuery: [ - 'data' => <<<'DATA' - [out:json][timeout:180]; - area(3601784663)->.a; - ( - nwr["parking_space"="disabled"](area.a); - nwr["capacity:disabled"]["capacity:disabled"!~"^(no|0)$"](area.a); - ); - out geom tags; - DATA, - ], )] final class HandicapParking extends AbstractSource { diff --git a/src/SourceReader/SourceReader.php b/src/SourceReader/SourceReader.php index ecfa362..2c90212 100644 --- a/src/SourceReader/SourceReader.php +++ b/src/SourceReader/SourceReader.php @@ -23,8 +23,8 @@ public function read(SourceInterface $source): iterable // @todo Cache request responses. $definition = $source->definition; - return $this->client->request('GET', $definition->accessUrl, [ - 'query' => $definition->accessUrlQuery, + return $this->client->request('GET', $definition->accessUrlBase(), [ + 'query' => $definition->accessUrlQuery(), ])->toArray(); } diff --git a/src/Test/Command/SourceFetchContentCommand.php b/src/Test/Command/SourceFetchContentCommand.php index 6edaa73..97753b3 100644 --- a/src/Test/Command/SourceFetchContentCommand.php +++ b/src/Test/Command/SourceFetchContentCommand.php @@ -38,8 +38,8 @@ public function __invoke( try { $io->section($source); $sourceDefinition = $manager->getSource($definition->sourceId)->definition; - $url = $sourceDefinition->accessUrl; - $query = $sourceDefinition->accessUrlQuery; + $url = $sourceDefinition->accessUrlBase(); + $query = $sourceDefinition->accessUrlQuery(); $filename = preg_replace('@^[a-z]+://[^/]+/test/@', '', $definition->accessUrl); $filename = __DIR__.'/../../../tests/resources/'.$filename; From 7eb8ae3979cdf11a9f1ff979d430d28e8e76cd39 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 14:29:02 +0200 Subject: [PATCH 099/101] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ef5a6..9ef8a0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-28](https://github.com/itk-dev/enter/pull/28) + Aligned the test setup with the new importer approach * [PR-24](https://github.com/itk-dev/enter/pull/24) Refactored sources and releated services * [PR-25](https://github.com/itk-dev/enter/pull/25) From d6ef3cd19207f94e8d91e9c17ed5c85f39740e78 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 15 Sep 2026 14:37:20 +0200 Subject: [PATCH 100/101] Adjusted comment --- src/Source/Definition.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Source/Definition.php b/src/Source/Definition.php index 42a785f..6eda2f1 100644 --- a/src/Source/Definition.php +++ b/src/Source/Definition.php @@ -59,10 +59,6 @@ public static function of(string $class): self /** * The URL to request, without the query parameters. - * - * A source that has to spell out a long query — an Overpass QL script, - * say — declares it as a separate array rather than percent-encoding it - * into the URL by hand. */ public function accessUrlBase(): string { From 6d894d78bff79451bc8ef06cd7bcf00481a11d9c Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 15 Sep 2026 15:59:18 +0200 Subject: [PATCH 101/101] Cleaned up --- .github/workflows/project.yaml | 28 ++++++++++++++++++++++++++++ Taskfile.yml | 8 +++++++- src/Source/Osm/HandicapParking.php | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/project.yaml diff --git a/.github/workflows/project.yaml b/.github/workflows/project.yaml new file mode 100644 index 0000000..982a09a --- /dev/null +++ b/.github/workflows/project.yaml @@ -0,0 +1,28 @@ +name: Enter + +env: + COMPOSE_USER: runner + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + misc: + name: Miscellaneus + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: go-task/setup-task@v2 + + - name: Create docker network + run: docker network create frontend + + - name: Code analysis + run: task code-analysis + + - name: Run tests + run: task test diff --git a/Taskfile.yml b/Taskfile.yml index b781381..9ec546b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -39,6 +39,12 @@ tasks: - if: '[ "$TASK_USE_DDEV" = "true" ]' cmd: ddev launch + test:start: + desc: 'Start services needed to run test' + internal: true + cmds: + - docker compose up --detach phpfpm --no-deps + test: desc: Run all tests cmds: @@ -50,6 +56,7 @@ tasks: env: APP_ENV: test desc: 'Run all {{.TASK | replace "test:" "" }} tests' + deps: [test:start] cmd: task: php vars: @@ -191,7 +198,6 @@ tasks: desc: 'Run code analysis' cmds: - task: code-analysis:phpstan - - task: code-analysis:mago code-analysis:phpstan: desc: 'Run PHPStan' diff --git a/src/Source/Osm/HandicapParking.php b/src/Source/Osm/HandicapParking.php index 889ae0a..632e79e 100644 --- a/src/Source/Osm/HandicapParking.php +++ b/src/Source/Osm/HandicapParking.php @@ -97,7 +97,7 @@ public function createNgsiEntity(array $data, Wgs84Transformer $transformer): ?N ->setProperty('category', $this->category($tags)) ->setProperty('totalSpotNumber', $this->reservedBays($tags)) ->setProperty('source', $this->definition->accessUrl) - ->geoProperty('location', $this->transformer->transformGeometry($this->definition->crs, $geometry)) + ->geoProperty('location', $transformer->transformGeometry($this->definition->crs, $geometry)) // Add custom attributes ->additionalInformation([