diff --git a/assets/js/admin/element-import/OsmQueryBuilder.vue b/assets/js/admin/element-import/OsmQueryBuilder.vue
index 5d191ae99bf4fd915699401bcc390a6875c91b80..55e48a44aa6b751a9d39f73119714f72c0d933e8 100755
--- a/assets/js/admin/element-import/OsmQueryBuilder.vue
+++ b/assets/js/admin/element-import/OsmQueryBuilder.vue
@@ -58,13 +58,13 @@ export default {
                     }
                 }
                 queryString += this.$refs.boundsPicker.overpassQuery
-                if (query != '') result += `node${queryString};way${queryString};relation${queryString};`                  
+                if (query != '') result += `nwr${queryString};`                  
             }
             return result
         },
         overpassApiUrl() {
             // out meta provide extra data, out center provide center of way or relation
-            return `https://overpass-api.de/api/interpreter?data=[out:json][timeout:200];(${this.overpassQuery});out%20meta%20center;`
+            return `https://overpass-api.de/api/interpreter?data=[out:json][timeout:1000];(${this.overpassQuery});out%20meta%20center;`
         }
     },
     watch: {
diff --git a/assets/js/element-form/categories.js b/assets/js/element-form/categories.js
index 440b4fd45a88cfd0195658e2d5532daf278997aa..2843e5b6a6131b4d37f92aab386e04452f11bec8 100755
--- a/assets/js/element-form/categories.js
+++ b/assets/js/element-form/categories.js
@@ -7,6 +7,11 @@
 var index = 1;
 jQuery(document).ready(function()
 {	
+	// display parent if children is checked
+	$('.option-field.selected').each(function() {
+		$(this).parents('.option-field').addClass('selected')
+	})
+	
 	$(".category-select").change(function()
 	{ 
 		if (!$(this).val()) return;
@@ -16,10 +21,11 @@ jQuery(document).ready(function()
 		// if only single option, removing all others options laready selected
 		if ($(this).data('single-option'))
 		{
-			$(this).closest('.category-field').find('> .option-field:visible').each(function() { removeOptionField($(this)); });
+			$(this).closest('.category-field').find('> .option-field.selected').each(function() { removeOptionField($(this)); });
 		}
 		
 		var optionField = $('#option-field-' + $(this).val());
+		optionField.addClass('selected')
 		optionField.stop(true,false).slideDown({ duration: 350, easing: "easeOutQuart", queue: false, complete: function() {$(this).css('height', '');}});
 		optionField.attr('data-index', index);
 		optionField.css('-webkit-box-ordinal-group', index);
@@ -27,6 +33,7 @@ jQuery(document).ready(function()
 		optionField.css('-ms-flex-order', index);
 		optionField.css('-webkit-order', index);
 		optionField.css('order', index);
+		
 
 		checkForSelectLabel(optionField, 1);
 		index++;
@@ -47,7 +54,7 @@ jQuery(document).ready(function()
 			optionFieldToRemove.hide();
 		else
 			optionFieldToRemove.stop(true,false).slideUp({ duration: 350, easing: "easeOutQuart", queue: false, complete: function() {$(this).css('height', '');}});
-
+		optionFieldToRemove.removeClass('selected')
 		checkForSelectLabel(optionFieldToRemove, 0);
 	}
 
@@ -56,7 +63,7 @@ jQuery(document).ready(function()
 		var categorySelect = optionField.siblings('.category-field-select');
 		var select = categorySelect.find('input.select-dropdown');
 
-		if (optionField.siblings('.option-field:visible').length + increment === 0)
+		if (optionField.siblings('.option-field.selected').length + increment === 0)
 			select.val("Choisissez " + categorySelect.attr('data-picking-text'));
 		else
 			select.val("Ajoutez " + categorySelect.attr('data-picking-text'));
@@ -67,7 +74,7 @@ function encodeOptionValuesIntoHiddenInput()
 {
 	var optionValues = [];
 
-	$('.option-field:visible').each(function() 
+	$('.option-field.selected').each(function() 
 	{
 		var option = {};
 		option.id = $(this).attr('data-id');
diff --git a/assets/js/element-form/geocode-address.js b/assets/js/element-form/geocode-address.js
index 52aa4f4f35d8e34cb678819c908f89b58a73a254..d9bbddbb0d4846e8ed7350720d994516bde7fe5c 100755
--- a/assets/js/element-form/geocode-address.js
+++ b/assets/js/element-form/geocode-address.js
@@ -2,6 +2,7 @@ var geocoderJS;
 var geocodingProcessing = false;
 var firstGeocodeDone = false;
 var geocodedFormatedAddress = '';
+var geocodeResult = null;
 
 function getInputAddress() { return $('#input-address').val(); }
 
@@ -46,10 +47,12 @@ function geocodeAddress(address) {
 		if (results !== null && results.length > 0)
 		{
 			firstGeocodeDone = true;
+			geocodeResult = results[0];
 			map.setView(results[0].getCoordinates(), 18);
 			createMarker(results[0].getCoordinates());
 
 			console.log("Geocode result :", results[0]);
+			$(window).trigger('geocoded', results[0]);
 
 			// Detect street address when geocoder fails to retrieve it (OSM case)
 			var patt = new RegExp(/^\d+/g);
diff --git a/assets/js/element-form/init-map.js b/assets/js/element-form/init-map.js
index 07876bc4dd4267583958bc6c93748f8342a63de7..dd7bb0c564465e4a7620f762bf12c9fb36efc488 100755
--- a/assets/js/element-form/init-map.js
+++ b/assets/js/element-form/init-map.js
@@ -42,12 +42,11 @@ function createMarker(position)
 	marker = new L.Marker(position, { draggable: true } ).addTo(map);
 	marker.on('dragend', function()
 	{
-	    $('#input-latitude').attr('value',marker.getLatLng().lat);
+	  	$('#input-latitude').attr('value',marker.getLatLng().lat);
 		$('#input-longitude').attr('value',marker.getLatLng().lng);
   	});
-	
-	// TODO translate message TEST
-	marker.bindPopup(`<center>${$('.translate').data('move-me')}</center>`).openPopup();
+
+  	marker.bindPopup(`<center>${t('element-form.geocoded-marker-text')}</center>`).openPopup();
 }
 
 function fitBounds(rawbounds)
diff --git a/assets/js/element-form/initialisation.js b/assets/js/element-form/initialisation.js
index b5743fdb7a63ff9b4b4de392b61b7b11e6165be9..6e151a9d8f8dffb7dcc6cd1beb017a5848b32333 100755
--- a/assets/js/element-form/initialisation.js
+++ b/assets/js/element-form/initialisation.js
@@ -1,6 +1,17 @@
+$('.to-html:not(.initialized)').each(function() { $(this).html($(this).text()).addClass('initialized'); });
+
 jQuery(document).ready(function()
 {
-  $('select:not(.select2)').material_select();
+  function handleSelectInitialized($select) {
+    $select.closest('.input-field').addClass("initialized")
+    $select.closest('.input-field').find('.material-icons').addClass('active');
+  }
+  $('select:not(.select2)').each(function() {
+    if ($(this).val()) handleSelectInitialized($(this))
+  })
+  $('select:not(.select2)').on('change', function() {
+    handleSelectInitialized($(this))
+  }).material_select()
 
   $('select.select2').change(function() {
     // add relevant classes to prefix icon, to it's colored the same way than other fields
@@ -17,10 +28,11 @@ jQuery(document).ready(function()
   })
   $('select.select2').trigger('change');
   // add relevant classes to prefix icon, to it's colored the same way than other fields
-  $('.select2-search__field').focus(function() { $(this).closest('.input-field').find('.material-icons').addClass('active') })
+  $('input.select-dropdown, .select2-search__field').focus(function() { $(this).closest('.input-field').find('.material-icons').addClass('active') })
   $('.select2-search__field').blur(function() { $(this).closest('.input-field').find('.material-icons').removeClass('active') })
+  $('input.select-dropdown').blur(function() { if (!$(this).val()) $(this).closest('.input-field').find('.material-icons').removeClass('active') })
 
-  $('.to-html').each(function() { $(this).html($(this).text()); });
+  $('.to-html:not(.initialized)').each(function() { $(this).html($(this).text()).addClass('initialized'); });
 
   // TIMEPICKERS
   $('.timepicker').each(function(e) {
diff --git a/assets/js/home.js b/assets/js/home.js
index 54c6cb1f328aeebda6f717b689f4c0eec1384dbd..6426ebf47314cf25f45e661bab34e594f4805133 100755
--- a/assets/js/home.js
+++ b/assets/js/home.js
@@ -12,7 +12,7 @@ $(document).ready(function()
 	carto = goGoCarto('#gogocarto', gogoJsConf);
 
 	// on search submit, redirect to the route provided by gogocartoJs
-	$('.search-bar').on('searchRoute', function(evt, route) {
+  $('.search-bar').on('searchRoute', function(evt, route) {
 		var mainOption;
 		// in small screen a select is displayed
 		if ($('.category-field-select').is(':visible'))
@@ -29,7 +29,7 @@ $(document).ready(function()
 		if (mainOption) route += '?cat=' + mainOption;
 		var path = window.location.pathname + '/annuaire' + route;
 		window.location.href = window.location.origin + path.replace('//', '/');
-	})
+  })
 
 	// clear viewport and address cookies
 	eraseCookie('viewport');
diff --git a/assets/js/i18n.js b/assets/js/i18n.js
new file mode 100644
index 0000000000000000000000000000000000000000..0231c66d169abc3a16e50f1647bea7d240006c94
--- /dev/null
+++ b/assets/js/i18n.js
@@ -0,0 +1,25 @@
+var gogoLocale = 'fr'; // overwritten in layout templates using config.locale
+var gogoFallbackLocale = 'en';
+
+// Use this function anywhere
+// handle interpolation : t('helo.world', {user: "Seby"})
+function t(key, params) {
+    var result = gogoTrans(gogoLocale + '.' + key, params)
+    if (!result) result = gogoTrans(gogoFallbackLocale + '.' + key, params)
+    if (!result) result = key
+    return result
+}
+
+function gogoTrans(key, params) {
+    result = gogoI18n // gogoI18n is defined in web/js/javascript-translations.js
+    path = key.split('.')
+    for(var i = 0; i < path.length; i++) {
+        result = result[path[i]] || {}
+    }
+    if (typeof result == 'string') {
+        for(var paramKey in params) {
+            result = result.replace('{' + paramKey +'}', params[paramKey])
+        }
+    }  
+    return !result.length ? undefined : result
+}
\ No newline at end of file
diff --git a/assets/scss/components/element-form/_category-field.scss b/assets/scss/components/element-form/_category-field.scss
index f96ab8db17d0df2bc63d031771bb85c0c3c19bf9..588b8b927f9783db389749f47b7cb4c1bd7fc846 100755
--- a/assets/scss/components/element-form/_category-field.scss
+++ b/assets/scss/components/element-form/_category-field.scss
@@ -88,6 +88,8 @@
 		@include flex(initial);
 		margin-bottom: 1rem;
 
+		&:not(.selected) { display: none !important; }
+
 		&.inline 
 		{ 
 			display: inline-block;
diff --git a/assets/scss/element-form.scss b/assets/scss/element-form.scss
index f349a9c30c3a9e0aa9be6d030aece4c0e005f1e7..d35a6d7d84f5770e5ba6ddba018804013c1f5e99 100755
--- a/assets/scss/element-form.scss
+++ b/assets/scss/element-form.scss
@@ -43,6 +43,12 @@
     color: inherit;
     font-weight: bold;
   }
+  .select-wrapper .caret {
+    opacity: .6;
+  }
+  .select-input:not(.initialized) {
+    label { opacity: 0; }
+  }
   .mandatory-asterisk:after {
     content: "*";
     margin-left: 5px;
@@ -104,6 +110,7 @@
   }
 
   .character-counter { margin-top: -10px; }
+  .option-field-description .character-counter { margin-top: 0; }
   textarea ~ .character-counter { margin-top: -15px; }
 
   .invalid-required {
@@ -203,11 +210,6 @@
     @media screen and (min-width: 600px) { margin-bottom: -10px !important; }
   }
 
-  #input-website
-  {
-    margin-bottom: 2rem;
-  }
-
   .logged-info
   {
     margin-top: 1rem;
@@ -518,4 +520,8 @@
     max-width: 800px;
     margin: 0 auto;
   }
+
+  .to-html:not(.initialized) {
+    visibility: hidden;
+  }
 }
\ No newline at end of file
diff --git a/assets/scss/partials/_flash-messages.scss b/assets/scss/partials/_flash-messages.scss
index d8382546a823862e691e5dd5d030bdbaf568cf6c..60167f2d5ad287b2973f130e2e93f8441cb9b500 100755
--- a/assets/scss/partials/_flash-messages.scss
+++ b/assets/scss/partials/_flash-messages.scss
@@ -36,7 +36,7 @@
         display: none;
     }
 
-    &.notice { background-color: #444444; a:hover { color: $primary-color; } }
+    &.notice { background-color: transparent; a:hover { color: $primary-color; } }
     &.error { background-color: #b90303; a:hover { color: white; } }
     &.success { background-color: #4caf50; a:hover { color: #444444; } }
 }
diff --git a/composer.lock b/composer.lock
index ff1c4216c5b15870b2308b3eb33f6ba0955a5d79..e0ba0831b06b3d1ae10b84ba5b48db37578be88b 100755
--- a/composer.lock
+++ b/composer.lock
@@ -1284,7 +1284,7 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/seballot/mongodb-odm.git",
-                "reference": "278dd98d6491c5380db6902024b860e1ca8f8450"
+                "reference": "84d24760bd3a55f3f4da78d1cc52aa28fbb50516"
             },
             "require": {
                 "doctrine/annotations": "~1.2",
@@ -1363,7 +1363,7 @@
                 "odm",
                 "persistence"
             ],
-            "time": "2021-01-30T08:22:06+00:00"
+            "time": "2021-04-10T06:59:56+00:00"
         },
         {
             "name": "doctrine/mongodb-odm-bundle",
@@ -11373,5 +11373,6 @@
     "platform": {
         "php": "^7.3"
     },
-    "platform-dev": []
+    "platform-dev": [],
+    "plugin-api-version": "2.0.0"
 }
diff --git a/config/parameters.yaml b/config/parameters.yaml
index aeaf71d7e7303cab2d8830e9c6421fdecd054bae..66cda5dbb6c9df68a6bba5e2f3a41bc2e634b848 100755
--- a/config/parameters.yaml
+++ b/config/parameters.yaml
@@ -1,7 +1,7 @@
 # Put parameters here that don't need to change on each machine where the app is deployed
 # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
 parameters:
-    app.version: 3.2.6
+    app.version: 3.2.7
     locale: fr
     router.request_context.host: '%env(string:BASE_URL)%'
     from_email: "%env(FROM_EMAIL)%"
diff --git a/docker-compose.yml b/docker-compose.yml
old mode 100755
new mode 100644
index 355f77e5760513ca464e66ae61d72f3772969e42..9c198d10e1c542d4042f07ee2903d73635ea1868
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -9,13 +9,11 @@ services:
       - mongo
     container_name: gogocarto
     volumes:      
-      - .:/var/www:cached
+      - .:/var/www
       - /var/www/var/cache
       - /var/www/var/log
       - /var/www/var/sessions
       - /var/www/web/uploads
-      - ./vendor:/var/www/html/vendor:delegated
-      - ./node_modules:/var/www/html/node_modules:delegated
     ports:
       - "3008:80"
     links:
diff --git a/gulpfile.js b/gulpfile.js
index 6bfb7024261ef231b2adac76bc33dcb967feabdf..87f3fe6bc1719f4c0513455d06167efe4201d807 100755
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -6,6 +6,8 @@ const gulp = require('gulp'),
     concat = require('gulp-concat'),
     gzip = require('gulp-gzip'),
     del = require('del'),
+    yaml = require('gulp-yaml'),
+    header = require('gulp-header'),
     workboxBuild = require('workbox-build');
 
 const scriptsHome = () =>
@@ -24,12 +26,21 @@ const scriptsElementForm = () =>
     .pipe(gulp.dest('web/js'));
 
 const scriptsLibs = () => {
-  const gogocarto = gulp.src(['node_modules/gogocarto-js/dist/gogocarto.js', 'assets/js/init-sw.js', 'custom/**/*.js'])
+  const translations = gulp.src('translations/javascripts-translations.yaml')
+    .pipe(yaml({schema: 'DEFAULT_SAFE_SCHEMA', ext: '.js'}))
+    .pipe(header("var gogoI18n = "))
+    .pipe(gulp.dest('web/js'))
+  const gogocarto = gulp.src([
+      'node_modules/gogocarto-js/dist/gogocarto.js', 
+      'assets/js/init-sw.js', 
+      'custom/**/*.js',
+      'assets/js/i18n.js',
+      'web/js/javascripts-translations.js'])
     .pipe(concat('gogocarto.js'))
     .pipe(gulp.dest('web/js'));
   const sw = gulp.src(['assets/js/vendor/**/*'])
     .pipe(gulp.dest('web/js'));
-  return merge(gogocarto, sw);
+  return merge(translations, gogocarto, sw);
 };
 
 const serviceWorker = async () => {
@@ -100,10 +111,10 @@ exports.watch = () => {
   gulp.watch(['assets/js/**/*.js', '!assets/js/element-form/**/*.js'],
               gulp.series(scriptsExternalPages, serviceWorker));
 
-  gulp.watch(['node_modules/gogocarto-js/dist/**/*'],
+  gulp.watch(['node_modules/gogocarto-js/dist/**/*', 'custom/**/*.css'],
               gulp.series(gogocarto_assets, serviceWorker));
 
-  gulp.watch(['assets/js/vendor/**/*.js','assets/js/admin/**/*.js', 'node_modules/gogocarto-js/dist/gogocarto.js'],
+  gulp.watch(['assets/js/vendor/**/*.js','assets/js/admin/**/*.js', 'node_modules/gogocarto-js/dist/gogocarto.js', 'custom/**/*.js', 'assets/js/i18n.js', 'translations/javascripts-translations.yaml'],
               gulp.series(scriptsLibs, serviceWorker));
 
   gulp.watch(['assets/js/home.js'], gulp.series(scriptsHome, serviceWorker));
@@ -118,3 +129,5 @@ const cleanJs = () =>
 exports.build = gulp.series(cleanJs, cleanCss, gulp.parallel(stylesBuild, scriptsLibs, scriptsHome, scriptsExternalPages, scriptsElementForm, gogocarto_assets), serviceWorker);
 
 exports.production = gulp.parallel(gulp.series(prod_styles, gzip_styles), gulp.series(prod_js, gzip_js));
+
+exports.libs = gulp.series(scriptsLibs)
\ No newline at end of file
diff --git a/package.json b/package.json
index 35b5dffd185dfb333aa4276a8e201e720526ba5d..e30483ebe44c96c30e9ce00509fa1fec50ecc634 100755
--- a/package.json
+++ b/package.json
@@ -33,7 +33,9 @@
   },
   "dependencies": {
     "core-js": "^3.8.1",
-    "gogocarto-js": "^1.7.2",
+    "gogocarto-js": "^1.7.3",
+    "gulp-header": "^2.0.9",
+    "gulp-yaml": "^2.0.4",
     "leaflet": "^1.7.1",
     "leaflet-shades": "^0.1.4",
     "natives": "^1.1.6",
diff --git a/src/Admin/CategoryAdmin.php b/src/Admin/CategoryAdmin.php
index 344d665918697f190536276e01263a848938973c..641f8ffc24d711a2cc5f48414c07e5f59a957bb2 100755
--- a/src/Admin/CategoryAdmin.php
+++ b/src/Admin/CategoryAdmin.php
@@ -50,7 +50,7 @@ class CategoryAdmin extends GoGoAbstractAdmin
         }
 
         $formMapper
-          ->panel('primary', ['class' => 'col-xs-12 col-md-6'])
+          ->halfPanel('primary')
             ->add('name', null, ['required' => true])
             ->add('pickingOptionText', null, ['required' => true])
             ->add('parent', ModelType::class, [
@@ -60,28 +60,29 @@ class CategoryAdmin extends GoGoAbstractAdmin
             ->add('isMandatory')
             ->add('singleOption')
             ->add('enableDescription')
-            ->end()
-            ->panel('secondary', ['class' => 'col-xs-12 col-md-6', 'box_class' => 'box'])
+            ->add('descriptionLabel')
+          ->end()
+          ->halfPanel('secondary')
              ->add('nameShort')
              ->add('index')
              ->add('showExpanded')
                    ->add('unexpandable')
              ->add('displaySuboptionsInline')
-        ->end()
-      ->panel('display', ['class' => 'col-md-6', 'box_class' => 'box'])
-         ->add('displayInMenu')
-         ->add('displayInInfoBar')
-         ->add('displayInForm')
-      ->end()
-        ->panel('categories', array('class' => 'col-xs-12 sub-options-container'))
-        	->add('isFixture', HiddenType::class, ['attr' => ['class' => 'gogo-sort-options'], 'label_attr' => ['style' => 'display:none']])
-      ->add('options', CollectionType::class, array(
-          'by_reference' => false,
-          'entry_type' => OptionLiteType::class,
-          'allow_add' => true,
-          'label_attr'=> ['style'=> 'display:none']))
-        ->end()
-    ;
+          ->end()
+          ->halfPanel('display', ['class' => 'col-md-6', 'box_class' => 'box'])
+            ->add('displayInMenu')
+            ->add('displayInInfoBar')
+            ->add('displayInForm')
+          ->end()
+          ->panel('categories', array('class' => 'col-xs-12 sub-options-container'))
+            ->add('isFixture', HiddenType::class, ['attr' => ['class' => 'gogo-sort-options'], 'label_attr' => ['style' => 'display:none']])
+            ->add('options', CollectionType::class, array(
+            'by_reference' => false,
+            'entry_type' => OptionLiteType::class,
+            'allow_add' => true,
+            'label_attr'=> ['style'=> 'display:none']))
+          ->end()
+        ;
     }
 
     protected function configureListFields(ListMapper $listMapper)
diff --git a/src/Admin/ConfigurationDuplicatesAdmin.php b/src/Admin/ConfigurationDuplicatesAdmin.php
index b4c46d6ee9a4093bc7dd13b6a170415dc886b6bd..743c4cbe4f884ad0a0843b48f8ad2682a37ddeff 100755
--- a/src/Admin/ConfigurationDuplicatesAdmin.php
+++ b/src/Admin/ConfigurationDuplicatesAdmin.php
@@ -36,6 +36,7 @@ class ConfigurationDuplicatesAdmin extends ConfigurationAbstractAdmin
 
         $sourceList = $dm->query('Element')->distinct('sourceKey')->getArray();
         $sourceList = array_merge($sourceList, $dm->query('Import')->distinct('sourceName')->getArray());
+        $sourceList = array_unique($sourceList);
         // Remove no more used sources
         $priorityList = $this->getSubject()->getDuplicates()->getSourcePriorityInAutomaticMerge();
         $newPriorityList = [];
@@ -65,6 +66,25 @@ class ConfigurationDuplicatesAdmin extends ConfigurationAbstractAdmin
                         'class' => 'gogo-source-priority',
                         'data-source-list' => $sourceList]])
             ->end()
+
+            ->with('Restreindre la détection manuelle (optionel)', ['box_class' => 'box box-default'])
+                ->add('duplicates.sourcesToDetectFrom', ChoiceType::class, [
+                    'label' => "Chercher les doublons entre les sources (laisser vide pour chercher dans toute la base de donnée)",
+                    'choice_label' => function ($choice, $key, $value) {
+                        if ('' === $choice) return 'Cette carte';  
+                        return $choice;
+                    },
+                    'choices' => $sourceList,
+                    'multiple' => true, 'required' => false])
+                ->add('duplicates.sourcesToDetectWith', ChoiceType::class, [
+                    'label' => "Et les sources (laisser vide pour chercher dans toute la base de donnée)",
+                    'choices' => $sourceList,
+                    'choice_label' => function ($choice, $key, $value) {
+                        if ('' === $choice) return 'Cette carte';              
+                        return $choice;
+                    },
+                    'multiple' => true, 'required' => false])
+            ->end()
         ;
     }
 }
diff --git a/src/Controller/Admin/BulkActions/BulkActionsAbstractController.php b/src/Controller/Admin/BulkActions/BulkActionsAbstractController.php
index a92d2d7a6f64c1b03b349b97a38691cafe21cce8..f5dc9f3c6c667b593bb1b31876eaf45266bcf6c7 100755
--- a/src/Controller/Admin/BulkActions/BulkActionsAbstractController.php
+++ b/src/Controller/Admin/BulkActions/BulkActionsAbstractController.php
@@ -27,6 +27,7 @@ class BulkActionsAbstractController extends Controller
         $qb = $dm->query('Element')
             ->field('status')->gte(ElementStatus::PendingModification)
             ->skip($batchFromStep);
+        $qb = $this->filterElements($qb);
         $count = (clone $qb)->getCount();
         $elementsToProcceedCount = 0;
         if ($count > $this->batchSize) {
@@ -81,6 +82,11 @@ class BulkActionsAbstractController extends Controller
             'title' => $this->title ? $this->title : $functionToExecute, ]);
     }
 
+    protected function filterElements($qb)
+    {
+        return $qb;
+    }
+
     protected function redirectToIndex()
     {
         return $this->redirect($this->generateUrl('gogo_bulk_actions_index'));
diff --git a/src/Controller/Admin/BulkActions/DuplicatesDetectionController.php b/src/Controller/Admin/BulkActions/DuplicatesDetectionController.php
index 13d64a353be407d5e127af174bab9f0fd0622a93..10411845feaa12a1b478e4469a00e9faba7c816e 100755
--- a/src/Controller/Admin/BulkActions/DuplicatesDetectionController.php
+++ b/src/Controller/Admin/BulkActions/DuplicatesDetectionController.php
@@ -35,9 +35,16 @@ class DuplicatesDetectionController extends BulkActionsAbstractController
         return $this->elementsBulkAction('detectDuplicates', $dm, $request, $session, $t);
     }
 
+    protected function filterElements($qb)
+    {
+        if (count($this->config->getDuplicates()->getSourcesToDetectFrom()) > 0)
+            $qb->field('sourceKey')->in($this->config->getDuplicates()->getSourcesToDetectFrom());
+        return $qb;
+    }
+
     public function detectDuplicates($element, $dm)
     {
-        $result = $this->duplicateService->detectDuplicatesFor($element);
+        $result = $this->duplicateService->detectDuplicatesFor($element, $this->config->getDuplicates()->getSourcesToDetectWith());
         if ($result === null) return "";
         return $this->render('admin/pages/bulks/bulk_duplicates.html.twig', [
             'duplicates' => [$result['elementToKeep'], $result['duplicate']],
diff --git a/src/Controller/Admin/ElementAdminController.php b/src/Controller/Admin/ElementAdminController.php
index 7971b16351843f6f9495c9168126a4efe8a13990..038fdb39b9e822e449b6b40941dbbaf0b6c22594 100755
--- a/src/Controller/Admin/ElementAdminController.php
+++ b/src/Controller/Admin/ElementAdminController.php
@@ -170,7 +170,8 @@ class ElementAdminController extends ElementAdminBulkController
         $newData = [];
         if ($request->get('data'))
             foreach($request->get('data') as $key => $value) {
-                $newData[slugify($key, false)] = $value;
+                // array data is displayed with json_encode, so we decode it when saving
+                $newData[slugify($key, false)] = json_decode($value) ?? $value;
             }
         $element->setCustomData($newData);
         $adr = $request->get('address');
diff --git a/src/Controller/Admin/ImportAdminController.php b/src/Controller/Admin/ImportAdminController.php
index 1c8bb488d6b71def4de4bd6fd2e40f7deefe5908..4db5cd5af0c8566a8421bb2f35168d5684c0f6ef 100755
--- a/src/Controller/Admin/ImportAdminController.php
+++ b/src/Controller/Admin/ImportAdminController.php
@@ -161,6 +161,7 @@ class ImportAdminController extends Controller
                                         $mappedCategories[$key] = $categoriesCreated[$categoryId];
                                     } else {
                                         $fieldName = $currentTaxonomyMapping[$originName]['fieldName'];
+                                        if (startsWith($fieldName, 'category_')) $fieldName = str_replace('category_', '', $fieldName);
                                         if (array_key_exists($fieldName, $createdParent))
                                             $parent = $createdParent[$fieldName];
                                         else
diff --git a/src/Controller/ElementFormController.php b/src/Controller/ElementFormController.php
index 98d3053072af97de739bd9dfaa1ae7977b23ae17..82f8b3cac00040f8e0064143e9049115758665d2 100755
--- a/src/Controller/ElementFormController.php
+++ b/src/Controller/ElementFormController.php
@@ -340,6 +340,7 @@ class ElementFormController extends GoGoController
                         'config' => $configService->getConfig(),
                         'imagesMaxFilesize' => $this->detectMaxUploadFileSize('images'),
                         'filesMaxFilesize' => $this->detectMaxUploadFileSize('files'),
+                        'isOwner' => $isUserOwnerOfValidElement
                     ]);
     }
 
diff --git a/src/Document/Category.php b/src/Document/Category.php
index b6b00fa14094b894f70e889d27de98c8877a5a3b..db8a78f4ac010f76ed146b25de39992f2cb77cc6 100755
--- a/src/Document/Category.php
+++ b/src/Document/Category.php
@@ -81,6 +81,13 @@ class Category
      */
     private $enableDescription = false;
 
+    /**
+     * @var string
+     * @Exclude(if="object.getEnableDescription() == false")
+     * @MongoDB\Field(type="string")
+     */
+    private $descriptionLabel = '';
+
     /**
      * @var bool
      * @Exclude(if="object.getDisplayInMenu() == true")
@@ -726,4 +733,28 @@ class Category
     {
         return $this->customId;
     }
+
+    /**
+     * Get the value of descriptionLabel
+     *
+     * @return  string
+     */ 
+    public function getDescriptionLabel()
+    {
+        return $this->descriptionLabel;
+    }
+
+    /**
+     * Set the value of descriptionLabel
+     *
+     * @param  string  $descriptionLabel
+     *
+     * @return  self
+     */ 
+    public function setDescriptionLabel($descriptionLabel)
+    {
+        $this->descriptionLabel = $descriptionLabel;
+
+        return $this;
+    }
 }
diff --git a/src/Document/Configuration/ConfigurationDuplicates.php b/src/Document/Configuration/ConfigurationDuplicates.php
index 7d6946fe3d28727d9bf362c2f02b33dc04612085..3a40992e8345f8b1d212274b37a0ecebf106518d 100755
--- a/src/Document/Configuration/ConfigurationDuplicates.php
+++ b/src/Document/Configuration/ConfigurationDuplicates.php
@@ -32,6 +32,12 @@ class ConfigurationDuplicates
      */
     private $currentProcessState;
 
+    /**  @MongoDB\Field(type="collection") */
+    private $sourcesToDetectFrom = [];
+
+    /**  @MongoDB\Field(type="collection") */
+    private $sourcesToDetectWith = [];
+
     private $formFields = [];
     private $fieldsInvolvded = null;
 
@@ -197,4 +203,44 @@ class ConfigurationDuplicates
 
         return $this;
     }
+
+    /**
+     * Get the value of sourcesToDetectFrom
+     */ 
+    public function getSourcesToDetectFrom()
+    {
+        return $this->sourcesToDetectFrom;
+    }
+
+    /**
+     * Set the value of sourcesToDetectFrom
+     *
+     * @return  self
+     */ 
+    public function setSourcesToDetectFrom($sourcesToDetectFrom)
+    {
+        $this->sourcesToDetectFrom = $sourcesToDetectFrom;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of sourcesToDetectWith
+     */ 
+    public function getSourcesToDetectWith()
+    {
+        return $this->sourcesToDetectWith;
+    }
+
+    /**
+     * Set the value of sourcesToDetectWith
+     *
+     * @return  self
+     */ 
+    public function setSourcesToDetectWith($sourcesToDetectWith)
+    {
+        $this->sourcesToDetectWith = $sourcesToDetectWith;
+
+        return $this;
+    }
 }
diff --git a/src/Document/Element.php b/src/Document/Element.php
index 71df4bbcedefa9548d8d49fefb7219fc3cc93bf9..a8b5e593878b1b8e110a5737b8d2a2d890ab7dec 100755
--- a/src/Document/Element.php
+++ b/src/Document/Element.php
@@ -398,7 +398,7 @@ class Element
     public function getOsmUrl($config)
     {
         if (!$this->isFromOsm()) return '';
-        return $config->getOsm()->getFormattedOsmHost() . $this->getProperty('osm/type') . '/' . $this->getOldId();
+        return $config->getOsm()->getFormattedOsmHost() . $this->getProperty('osm_type') . '/' . $this->getOldId();
     }
 
     public function getShowUrlFromController($router)
diff --git a/src/Document/Import.php b/src/Document/Import.php
index 51e3b463ed64b80d291f32d3f564b9a7defa0f55..8506c8b8fccf6b58c6c701c92b6f0319298c49d1 100755
--- a/src/Document/Import.php
+++ b/src/Document/Import.php
@@ -230,6 +230,11 @@ class Import extends AbstractFile
         if ($this->file) return 'csv';
     }
 
+    public function isHandlingCategories()
+    {
+        return count($this->taxonomyMapping) > 0 || count($this->optionsToAddToEachElement) > 0;
+    }
+
     /**
      * Get id.
      *
@@ -493,8 +498,8 @@ class Import extends AbstractFile
      */
     public function getFieldToCheckElementHaveBeenUpdated()
     {
-        if ($this->getSourceType() == 'osm') return 'osm/version'; // TODO translate ?
-        return $this->fieldToCheckElementHaveBeenUpdated ?? 'updateAt'; // TODO translate ?
+        if ($this->getSourceType() == 'osm') return 'osm_version';
+        return $this->fieldToCheckElementHaveBeenUpdated ?? 'updateAt';
     }
 
     /**
diff --git a/src/Repository/ElementRepository.php b/src/Repository/ElementRepository.php
index 34ff5b60ed1516df254df227af71660d84926c94..c04579574793307e9a09b8db755ef47a71d28ff9 100755
--- a/src/Repository/ElementRepository.php
+++ b/src/Repository/ElementRepository.php
@@ -23,7 +23,7 @@ class ElementRepository extends DocumentRepository
         return $this->config;
     }
     
-    public function findDuplicatesFor($element, $elementIdsToIgnore = [])
+    public function findDuplicatesFor($element, $elementIdsToIgnore = [], $sourceToDetectWith = [])
     {
         $forNewlyCreatedElement = $element->getId() == null;
 
@@ -50,6 +50,10 @@ class ElementRepository extends DocumentRepository
 
         $qb->field('id')->notIn(array_merge([$element->getId()], $element->getNonDuplicatesIds(), $elementIdsToIgnore));
         
+        if (count($sourceToDetectWith)) {
+            $qb->field('sourceKey')->in($sourceToDetectWith);
+        }
+        
         // Text Search
         $result1 = [];
         if ($config->getUseGlobalSearch()) {
diff --git a/src/Services/ElementDuplicatesService.php b/src/Services/ElementDuplicatesService.php
index 32b4e660d14abe66575f8b233030015314ac1a94..be4e9985ae75feb0c8e17633a239d98fd447a796 100755
--- a/src/Services/ElementDuplicatesService.php
+++ b/src/Services/ElementDuplicatesService.php
@@ -29,12 +29,12 @@ class ElementDuplicatesService
         return $this->dupConfig;
     }
 
-    public function detectDuplicatesFor($element)
+    public function detectDuplicatesFor($element, $restrictToSources = [])
     {       
         if ($element->getStatus() >= ElementStatus::PendingModification
         && !in_array($element->getId(), $this->duplicatesFound)
         && !$element->isPotentialDuplicate()) {
-            $duplicates = $this->dm->get('Element')->findDuplicatesFor($element, $this->duplicatesFound);
+            $duplicates = $this->dm->get('Element')->findDuplicatesFor($element, $this->duplicatesFound, $restrictToSources);
             if (count($duplicates) == 0) return null;
             // only keep two duplicates, so get easier to manage for the users (less complicated cases)
             // so we sort duplicates and keep first (best choice)
diff --git a/src/Services/ElementImportMappingOntologyService.php b/src/Services/ElementImportMappingOntologyService.php
index d81b51510b6015036e09d41409a3f61ff3360538..5dcdb65d980c1a8ea0049014526ea32f88c4ff6a 100755
--- a/src/Services/ElementImportMappingOntologyService.php
+++ b/src/Services/ElementImportMappingOntologyService.php
@@ -100,17 +100,21 @@ class ElementImportMappingOntologyService
             // handle some special cases
             if ($import->getSourceType() == 'osm') {
                 switch ($prop) {
-                    case 'source': $mappedProp = 'osm/source'; break; // we don't want to overide GoGoCarto source field with Osm field
-                    case 'opening_hours': $mappedProp = 'osm/opening_hours'; break;
-                    case 'version': $mappedProp = 'osm/version'; break;
-                    case 'timestamp': $mappedProp = 'osm/timestamp'; break;
-                    case 'type': $mappedProp = 'osm/type'; break;
+                    case 'source': $mappedProp = 'osm_source'; break; // we don't want to overide GoGoCarto source field with Osm field
+                    case 'opening_hours': $mappedProp = 'osm_opening_hours'; break;
+                    case 'version': $mappedProp = 'osm_version'; break;
+                    case 'timestamp': $mappedProp = 'osm_timestamp'; break;
+                    case 'type': $mappedProp = 'osm_type'; break;
+                    case 'osmUrl': $mappedProp = 'osm_url'; break;
                 }
             }
             // use alternative name, like lat instead of latitude
             if (!$mappedProp && array_key_exists($slugProp, $this->mappedCoreFields)) {
                 $mappedProp = $this->mappedCoreFields[$slugProp];
             }
+            if (!$mappedProp && startsWith($slugProp, 'category')) {
+                $mappedProp = 'categories';
+            }
             // Asign mapping
             $this->ontologyMapping[$fullProp] = [
                 'mappedProperty' => $mappedProp,
diff --git a/src/Services/ElementImportMappingService.php b/src/Services/ElementImportMappingService.php
index 4ec8ab2349486eeca4a1e7c853c100d2f1df9572..3d97cac56a6232e93d542513715c5719670b1788 100755
--- a/src/Services/ElementImportMappingService.php
+++ b/src/Services/ElementImportMappingService.php
@@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\DocumentManager;
 class ElementImportMappingService
 {
     protected $dm;
+    protected $config;
 
     public function __construct(DocumentManager $dm, 
                                 ElementImportMappingOntologyService $ontologyService,
@@ -18,6 +19,12 @@ class ElementImportMappingService
         $this->mappingTableIds = [];
     }
 
+    private function getConfig()
+    {
+        if (!$this->config) $this->config = $this->dm->get('Configuration')->findConfiguration();
+        return $this->config;
+    }
+
     public function transform($data, $import)
     {
         // Execute custom code (the <?php is used to have proper code highliting in text editor, we remove it before executing)
@@ -91,6 +98,7 @@ class ElementImportMappingService
                     unset($data[$key]['changeset']); 
                     unset($data[$key]['uid']); 
                     unset($data[$key]['user']); 
+                    $data[$key]['osmUrl'] = $this->getConfig()->getOsm()->getFormattedOsmHost() . "{$row['type']}/{$row['id']}";
                 }
             } else {
                 // the $row is not an array, probably a string so we ignore it
@@ -101,7 +109,7 @@ class ElementImportMappingService
         // Ontology
         $this->ontologyService->collectOntology($data, $import);
         $data = $this->ontologyService->mapOntology($data, $import);
-
+        
         if (null == $data || !is_array($data)) {
             return [];
         }
diff --git a/src/Services/ElementImportOneService.php b/src/Services/ElementImportOneService.php
index 435cba8b8eb568c88b00bd065b37d06e785b1314..9b0ff165dc36e92ea83ae20c24b02d751c524b6c 100755
--- a/src/Services/ElementImportOneService.php
+++ b/src/Services/ElementImportOneService.php
@@ -164,11 +164,12 @@ class ElementImportOneService
         if (0 == $lat || 0 == $lng) {
             $element->setModerationState(ModerationState::GeolocError);
         }
+        
         $element->setGeo(new Coordinates($lat, $lng));
         $this->createImages($element, $row);
         $this->createFiles($element, $row);
         $this->createOpenHours($element, $row);
-        unset($row['osm/opening_hours']);
+        unset($row['osm_opening_hours']);
         $this->saveCustomFields($element, $row);
 
         if ($updateExisting) {
@@ -283,10 +284,10 @@ class ElementImportOneService
 
     private function createOpenHours($element, $row)
     {
-        if(isset($row['osm/opening_hours'])) {
+        if(isset($row['osm_opening_hours'])) {
             try {
                 $oh = new OpenHours();
-                $oh->buildFromOsm($row['osm/opening_hours']);
+                $oh->buildFromOsm($row['osm_opening_hours']);
                 $element->setOpenHours($oh);                
             }
             catch(\Exception $e) {;}
@@ -298,6 +299,7 @@ class ElementImportOneService
 
     private function createOptionValues($element, $row, $import)
     {
+        if (!$import->isHandlingCategories()) return;
         $element->resetOptionsValues();
         $optionsIdAdded = [];
         $defaultOption = ['index' => 0, 'description' => ''];
diff --git a/src/Services/ElementImportService.php b/src/Services/ElementImportService.php
index 50ad17ec43f33f25edb947d673546a4a2def1ea7..469d271ba3be41c6f36178b76724d5a5b4aae16b 100755
--- a/src/Services/ElementImportService.php
+++ b/src/Services/ElementImportService.php
@@ -101,7 +101,7 @@ class ElementImportService
      */
     public function importJson($import, $onlyGetData = false)
     {
-        $json = file_get_contents(str_replace(' ', '%20', $import->getUrl()));
+        $json = file_get_contents(str_replace(' ', '%20', $import->getUrl()), 0, stream_context_create(["http"=>["timeout"=>3600]]));
         $data = json_decode($json, true);
         if (null === $data) return null;
         if ($onlyGetData) return $data;        
diff --git a/src/Services/ElementSynchronizationService.php b/src/Services/ElementSynchronizationService.php
index 07477998825fc26c46fb3d79c92c306052ef9db7..043e1f95d6d5a569081644ab67098b7255f0c6d9 100755
--- a/src/Services/ElementSynchronizationService.php
+++ b/src/Services/ElementSynchronizationService.php
@@ -44,65 +44,35 @@ class ElementSynchronizationService
                 // Init OSM API handler
                 $osm = $this->getOsmApiHandler();
                 $element = $contribution->getElement();
-                $osmFeature = $this->elementToOsm($element);
-                $osmFeaturesMainTags = $this->getOsmFeatureMainTags($osmFeature);
-                if (count($osmFeaturesMainTags) == 0) {
-                    return $promise->resolve(new Response(500, [], null, '1.1', "Cet élément n'a aucun des clés principales d'OpenStreetMap (amenity, shop...)")); // TODO translate
+                $gogoFeature = $this->elementToOsm($element);
+                $gogoFeaturesMainTags = $this->getMainTags($gogoFeature);
+                if (count($gogoFeaturesMainTags) == 0) {
+                    return $promise->resolve(new Response(500, [], null, '1.1', "Cet élément n'a aucun des clés principales d'OpenStreetMap (amenity, shop...)")); // TODO Translate
                 }
 
                 // Check contribution validity according to OSM criterias
-                if($this->allowOsmUpload($contribution, $preparedData)) {
+                if ($this->allowOsmUpload($contribution, $preparedData)) {
                     $toAdd = null;
 
                     // Process contribution
                     // New feature
-                    if($preparedData['action'] == 'add') {
-                        $toAdd = $osm->createNode($osmFeature['center']['latitude'], $osmFeature['center']['longitude'], $osmFeature['tags']);
+                    if ($preparedData['action'] == 'add') {
+                        $toAdd = $osm->createNode($gogoFeature['center']['latitude'], $gogoFeature['center']['longitude'], $gogoFeature['tags']);
                     }
                     // Edit existing feature
-                    else if($preparedData['action'] == 'edit') {
-                        $existingFeature = null;
-
-                        switch($element->getProperty('osm/type')) {
-                            case 'node':
-                                $existingFeature = $osm->getNode($osmFeature['osmId']);
-                                break;
-                            case 'way':
-                                $existingFeature = $osm->getWay($osmFeature['osmId']);
-                                break;
-                            case 'relation':
-                                $existingFeature = $osm->getRelation($osmFeature['osmId']);
-                                break;
-                        }
+                    else if ($preparedData['action'] == 'edit') {
+                        $osmFeature = null;
+                        $getType = "get".ucfirst($gogoFeature['type']);
+                        $osmFeature = $osm->$getType($gogoFeature['osmId']);
 
-                        if($existingFeature) {
+                        if ($osmFeature) {
                             // Check version number (to make sure Gogocarto version is the latest)
-                            if($existingFeature->getVersion() == intval($osmFeature['version'])) {
-                                // Edit tags
-                                foreach($osmFeature['tags'] as $k => $v) {
-                                    if($v == null || $v == '') {
-                                        $existingFeature->removeTag($k);
-                                    }
-                                    else {
-                                        $existingFeature->setTag($k, $v);
-                                    }
-                                }
-
-                                // If node coordinates are edited, check if it is detached
-                                if($element->getProperty('osm/type') == 'node' && (!$existingFeature->getWays()->valid() || $existingFeature->getWays()->count() == 0)) {
-                                    if($osmFeature['center']['latitude'] != $existingFeature->getLat()) {
-                                        $existingFeature->setLat($osmFeature['center']['latitude']);
-                                    }
-
-                                    if($osmFeature['center']['longitude'] != $existingFeature->getLon()) {
-                                        $existingFeature->setLon($osmFeature['center']['longitude']);
-                                    }
-                                }
-
-                                $toAdd = $existingFeature;
+                            if ($osmFeature->getVersion() == intval($gogoFeature['version'])) {
+                                if ($this->editOsmFeatureWithGoGoFeature($osmFeature, $gogoFeature))
+                                    $toAdd = $osmFeature;
                             }
                             else {
-                                $message = 'Feature versions mismatch: '.$osmFeature['version'].' on our side, '.$existingFeature->getVersion().' on OSM'; // TODO translate
+                                $message = 'Feature versions mismatch: '.$gogoFeature['version'].' on our side, '.$osmFeature->getVersion().' on OSM';
                                 return $promise->resolve(new Response(500, [], null, '1.1', $message));
                             }
                         }
@@ -113,16 +83,8 @@ class ElementSynchronizationService
                     }
 
                     // Create changeset and upload changes
-                    if(isset($toAdd)) {
-                        $changeset = $osm->createChangeset();
-                        $changeset->setId(-1); // To prevent bug with setTag
-                        $changeset->setTag('host', $this->urlService->generateUrl());
-                        $changeset->setTag('created_by:library', 'GoGoCarto');
-                        $changeset->setTag('created_by', $this->getConfig()->getAppName());
-                        $changeset->begin($this->getOsmComment($preparedData));
-
-                        // Add edited feature to changeset
-                        $changeset->add($toAdd);
+                    if (isset($toAdd)) {
+                        $changeset = $this->createsChangeSet($osm, $toAdd, $this->getOsmComment($preparedData));
 
                         // Close changeset
                         try {
@@ -131,30 +93,21 @@ class ElementSynchronizationService
                             // Update version in case of feature edit
                             $toUpdateInDb = null;
 
-                            if($preparedData['action'] == 'add') {
+                            if ($preparedData['action'] == 'add') {
                                 $toUpdateInDb = $osm->getNode($toAdd->getId());
                             }
-                            else if($preparedData['action'] == 'edit') {
-                                switch($element->getProperty('osm/type')) {
-                                    case 'node':
-                                        $toUpdateInDb = $osm->getNode($osmFeature['osmId']);
-                                        break;
-                                    case 'way':
-                                        $toUpdateInDb = $osm->getWay($osmFeature['osmId']);
-                                        break;
-                                    case 'relation':
-                                        $toUpdateInDb = $osm->getRelation($osmFeature['osmId']);
-                                        break;
-                                }
+                            else if ($preparedData['action'] == 'edit') {
+                                $toUpdateInDb = $osm->$getType($gogoFeature['osmId']);
                             }
 
-                            if($toUpdateInDb) {
-                                if($preparedData['action'] == 'add') {
-                                    $element->setCustomProperty('osm/type', 'node');
+                            if ($toUpdateInDb) {
+                                if ($preparedData['action'] == 'add') {
+                                    $element->setCustomProperty('osm_type', 'node');
                                     $element->setOldId($toUpdateInDb->getId());
                                 }
-                                $element->setCustomProperty('osm/version', $toUpdateInDb->getVersion());
-                                $element->setCustomProperty('osm/timestamp', strval($toUpdateInDb->getAttributes()->timestamp));
+                                $element->setCustomProperty('osm_url', $element->getOsmUrl($this->config));
+                                $element->setCustomProperty('osm_version', $toUpdateInDb->getVersion());
+                                $element->setCustomProperty('osm_timestamp', strval($toUpdateInDb->getAttributes()->timestamp));
                                 $this->dm->persist($element);
                                 $this->dm->flush();
                             }
@@ -180,29 +133,82 @@ class ElementSynchronizationService
         return $promise;
     }
 
+    private function editOsmFeatureWithGoGoFeature($osmFeature, $gogoFeature)
+    {
+        // Avoid empty commit : in gogocarto the update might be on field that are not sent to OSM
+        $isNewFeatureDifferentFromOldOne = false;
+
+        // Edit tags
+        $osmTags = $osmFeature->getTags();
+        foreach($gogoFeature['tags']  as $tagKey => $gogoTagValue) {
+            if (isset($osmTags[$tagKey]) && $osmTags[$tagKey] != $gogoTagValue) $isNewFeatureDifferentFromOldOne = true;
+        }
+
+        foreach($gogoFeature['tags'] as $k => $v) {
+            if ($v == null || $v == '') {
+                $osmFeature->removeTag($k);
+            }
+            else {
+                $osmFeature->setTag($k, $v);
+            }
+        }
+
+        // If node coordinates are edited, check if it is detached
+        if ($gogoFeature['type'] == 'node' && (!$osmFeature->getWays()->valid() || $osmFeature->getWays()->count() == 0)) {
+            if ($gogoFeature['center']['latitude'] != $osmFeature->getLat()) {
+                $osmFeature->setLat($gogoFeature['center']['latitude']);
+                $isNewFeatureDifferentFromOldOne = true;
+            }
+            if ($gogoFeature['center']['longitude'] != $osmFeature->getLon()) {
+                $osmFeature->setLon($gogoFeature['center']['longitude']);
+                $isNewFeatureDifferentFromOldOne = true;
+            }
+        }
+        
+        return $isNewFeatureDifferentFromOldOne;
+    }
+
+    private function createsChangeSet($osm, $feature, $comment)
+    {
+        $changeset = $osm->createChangeset();
+        $changeset->setId(-1); // To prevent bug with setTag
+        $changeset->setTag('host', $this->urlService->generateUrl());
+        $changeset->setTag('created_by:library', 'GoGoCarto');
+        $changeset->setTag('created_by', $this->getConfig()->getAppName());
+        $changeset->begin($comment);
+
+        // Add edited feature to changeset
+        $changeset->add($feature);
+
+        return $changeset;
+    }
+
     /**
      * Convert an element into a JSON-like OSM feature
      */
     public function elementToOsm(Element $element)
     {
         if (!$element->isSynchedWithExternalDatabase()) return null;
-        $osmFeature = [];
+        $gogoFeature = [];
 
         // Get original mappings
         $ontology = $element->getSource()->getOntologyMapping();
         $taxonomy = $element->getSource()->getTaxonomyMapping();
 
+        // Type
+        $gogoFeature['type'] = $element->getProperty('osm_type');
+
         // Coordinates
-        $osmFeature['center']['latitude'] = $element->getGeo()->getLatitude();
-        $osmFeature['center']['longitude'] = $element->getGeo()->getLongitude();
+        $gogoFeature['center']['latitude'] = $element->getGeo()->getLatitude();
+        $gogoFeature['center']['longitude'] = $element->getGeo()->getLongitude();
 
         // Categories
         foreach($taxonomy as $taxonomyKey => $taxonomyValue) {
             foreach($ontology as $ontologyKey => $ontologyValue) {
                 $ontologySubkeys = explode("/", $ontologyKey);
                 $ontologyTrueKey = array_pop($ontologySubkeys);
-                if($ontologyTrueKey == $taxonomyValue['fieldName']) {
-                    $this->setNestedArrayValue($osmFeature, $ontologyKey, $taxonomyKey, "/");
+                if ($ontologyTrueKey == $taxonomyValue['fieldName']) {
+                    $this->setNestedArrayValue($gogoFeature, $ontologyKey, $taxonomyKey, "/");
                 }
             }
         }
@@ -211,11 +217,11 @@ class ElementSynchronizationService
         $myCoreFields = array_diff($element::CORE_FIELDS, ['latitude', 'longitude', 'categories']);
         foreach($myCoreFields as $field) {
             $elemValue = $element->getProperty($field);
-            if(isset($elemValue)) {
+            if (isset($elemValue)) {
                 // Ontology
                 foreach($ontology as $ontologyKey => $ontologyValue) {
-                    if($ontologyValue['mappedProperty'] == $field) {
-                        $this->setNestedArrayValue($osmFeature, $ontologyKey, $elemValue, "/");
+                    if ($ontologyValue['mappedProperty'] == $field) {
+                        $this->setNestedArrayValue($gogoFeature, $ontologyKey, $elemValue, "/");
                         break;
                     }
                 }
@@ -225,19 +231,19 @@ class ElementSynchronizationService
         // Custom data
         foreach($element->getData() as $elemKey => $elemValue) {
             foreach($ontology as $ontologyKey => $ontologyValue) {
-                if($ontologyValue['mappedProperty'] == $elemKey) {
-                    $this->setNestedArrayValue($osmFeature, $ontologyKey, $elemValue, "/");
+                if ($ontologyValue['mappedProperty'] == $elemKey) {
+                    $this->setNestedArrayValue($gogoFeature, $ontologyKey, $elemValue, "/");
                     break;
                 }
             }
         }
 
         // Other data
-        $osmFeature['osmId'] = intval($element->getProperty('oldId'));
-        if($element->getOpenHours()) {
+        $gogoFeature['osmId'] = intval($element->getProperty('oldId'));
+        if ($element->getOpenHours()) {
             $h = $element->getOpenHours()->toOsm();
-            if(strlen($h) > 0) {
-                $osmFeature['tags']['opening_hours'] = $h;
+            if (strlen($h) > 0) {
+                $gogoFeature['tags']['opening_hours'] = $h;
             }
         }
 
@@ -246,8 +252,8 @@ class ElementSynchronizationService
         if (count($queries) == 1) {
             $query = $queries[0];
             foreach($query as $condition) {
-                if ($condition->operator == "=" && !isset($osmFeature['tags'][$condition->key]))
-                    $osmFeature['tags'][$condition->key] = $condition->value;
+                if ($condition->operator == "=" && !isset($gogoFeature['tags'][$condition->key]))
+                    $gogoFeature['tags'][$condition->key] = $condition->value;
             }
         }
         
@@ -256,7 +262,7 @@ class ElementSynchronizationService
             eval(str_replace('<?php', '', $element->getSource()->getCustomCodeForExport()));
         } catch (\Exception $e) {}
 
-        return $osmFeature;
+        return $gogoFeature;
     }
 
     /**
@@ -271,11 +277,11 @@ class ElementSynchronizationService
     {
         if ($this->linkElementToExistingOsmImport($element)) {
             // Get element in OSM format
-            $osmFeature = $this->elementToOsm($element);
-            $osmFeaturesMainTags = $this->getOsmFeatureMainTags($osmFeature);
+            $gogoFeature = $this->elementToOsm($element);
+            $gogoFeaturesMainTags = $this->getMainTags($gogoFeature);
 
             // If can't find main tags, do not send to OSM, feature might be broken
-            if(count($osmFeaturesMainTags) == 0) {
+            if (count($gogoFeaturesMainTags) == 0) {
                 return ['result' => false, 'duplicates' => []];
             }
             // Otherwise, start looking for duplicates
@@ -284,15 +290,15 @@ class ElementSynchronizationService
 
                 // Compute bounding box to retrieve
                 $radiusKm = self::OSM_SEARCH_RADIUS_METERS / 1000;
-                $north = $osmFeature['center']['latitude'] + ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI);
-                $east = $osmFeature['center']['longitude'] + ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI) / cos($osmFeature['center']['latitude'] * M_PI / 180);
-                $south = $osmFeature['center']['latitude'] - ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI);
-                $west = $osmFeature['center']['longitude'] - ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI) / cos($osmFeature['center']['latitude'] * M_PI / 180);
+                $north = $gogoFeature['center']['latitude'] + ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI);
+                $east = $gogoFeature['center']['longitude'] + ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI) / cos($gogoFeature['center']['latitude'] * M_PI / 180);
+                $south = $gogoFeature['center']['latitude'] - ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI);
+                $west = $gogoFeature['center']['longitude'] - ($radiusKm / self::EARTH_RADIUS) * (180 / M_PI) / cos($gogoFeature['center']['latitude'] * M_PI / 180);
 
                 // Load data from OSM editing API
                 $osm = $this->getOsmApiHandler();
                 $osm->get($west, $south, $east, $north);
-                $potentialDuplicates = $osm->search($osmFeaturesMainTags);
+                $potentialDuplicates = $osm->search($gogoFeaturesMainTags);
 
                 // Transform found potential duplicates into GogoCarto format$
                 foreach($potentialDuplicates as $dup) {
@@ -320,7 +326,7 @@ class ElementSynchronizationService
     /**
      * Extract the mains tags from the osm feature
      */
-    public function getOsmFeatureMainTags($osmFeature)
+    public function getMainTags($osmFeature)
     {
         // List tags to use for potential duplicates search
         $osmFeaturesMainTags = array_filter(
@@ -331,7 +337,7 @@ class ElementSynchronizationService
             ARRAY_FILTER_USE_KEY
         );
 
-        if(count($osmFeaturesMainTags) == 0) {
+        if (count($osmFeaturesMainTags) == 0) {
             $osmFeaturesMainTags = array_filter(
                 $osmFeature['tags'],
                 function($key) {
@@ -358,88 +364,38 @@ class ElementSynchronizationService
     {
         // Init OSM API handler
         $osm = $this->getOsmApiHandler();
-        $osmFeature = $this->elementToOsm($element);
-        $existingFeature = null;
-        $osmIdParts = explode('/', $osmId);
-
-        switch($osmIdParts[0]) {
-            case 'node':
-                $existingFeature = $osm->getNode($osmIdParts[1]);
-                break;
-            case 'way':
-                $existingFeature = $osm->getWay($osmIdParts[1]);
-                break;
-            case 'relation':
-                $existingFeature = $osm->getRelation($osmIdParts[1]);
-                break;
-        }
+        $gogoFeature = $this->elementToOsm($element);
+        $osmFeature = null;
+        $osmIdParts = explode('/', $osmId); // OSM id is as follow : type/ID <=> node/145236545
+        $gogoFeature['type'] = $osmIdParts[0];
+        $getType = 'get'.ucfirst($osmIdParts[0]);
+        $osmFeature = $osm->$getType($osmIdParts[1]);
 
-        if($existingFeature) {
-            // Edit tags
-            foreach($osmFeature['tags'] as $k => $v) {
-                if($v == null || $v == '') {
-                    $existingFeature->removeTag($k);
-                }
-                else {
-                    $existingFeature->setTag($k, $v);
-                }
-            }
-
-            // If node coordinates are edited, check if it is detached
-            if($osmIdParts[0] == 'node' && (!$existingFeature->getWays()->valid() || $existingFeature->getWays()->count() == 0)) {
-                if($osmFeature['center']['latitude'] != $existingFeature->getLat()) {
-                    $existingFeature->setLat($osmFeature['center']['latitude']);
-                }
+        if (!$osmFeature) return;
 
-                if($osmFeature['center']['longitude'] != $existingFeature->getLon()) {
-                    $existingFeature->setLon($osmFeature['center']['longitude']);
-                }
-            }
+        $somethingChanged = $this->editOsmFeatureWithGoGoFeature($osmFeature, $gogoFeature);
+        if (!$somethingChanged) return;
 
-            // Create changeset
-            $changeset = $osm->createChangeset();
-            $changeset->setId(-1); // To prevent bug with setTag
-            $changeset->setTag('host', $this->urlService->generateUrl());
-            $changeset->setTag('created_by:library', 'GoGoCarto');
-            $changeset->setTag('created_by', $this->getConfig()->getAppName());
-            $changeset->begin('Mise à jour attributs '.($existingFeature->getTag('name') ?? $existingFeature->getTag('brand') ?? $existingFeature->getTag('operator') ?? $osmId));
+        $comment = 'Mise à jour attributs '.($osmFeature->getTag('name') ?? $osmFeature->getTag('brand') ?? $osmFeature->getTag('operator') ?? $osmId);
+        $changeset = $this->createsChangeSet($osm, $osmFeature, $comment);
 
-            // Add edited feature to changeset
-            $changeset->add($existingFeature);
-
-            // Close changeset
-            try {
-                $changeset->commit();
-
-                // Update version in case of feature edit
-                $toUpdateInDb = null;
-                switch($osmIdParts[0]) {
-                    case 'node':
-                        $toUpdateInDb = $osm->getNode($osmIdParts[1]);
-                        break;
-                    case 'way':
-                        $toUpdateInDb = $osm->getWay($osmIdParts[1]);
-                        break;
-                    case 'relation':
-                        $toUpdateInDb = $osm->getRelation($osmIdParts[1]);
-                        break;
-                }
-
-                if($toUpdateInDb) {
-                    $element->setOldId($osmIdParts[1]);
-                    $element->setCustomProperty('osm/type', $toUpdateInDb->getType());
-                    $element->setCustomProperty('osm/version', $toUpdateInDb->getVersion());
-                    $element->setCustomProperty('osm/timestamp', strval($toUpdateInDb->getAttributes()->timestamp));
-                    $this->dm->persist($element);
-                    $this->dm->flush();
-                }
-            }
-            catch(\Exception $e) {
-                // Error when sending changeset
+        // Close changeset
+        try {
+            $changeset->commit();
+
+            // Update version in case of feature edit
+            if ($toUpdateInDb = $osm->$getType($osmIdParts[1])) {
+                $element->setOldId($osmIdParts[1]);
+                $element->setCustomProperty('osm_type', $toUpdateInDb->getType());
+                $element->setCustomProperty('osm_version', $toUpdateInDb->getVersion());
+                $element->setCustomProperty('osm_timestamp', strval($toUpdateInDb->getAttributes()->timestamp));
+                $element->setCustomProperty('osm_url', $element->getOsmUrl($this->config));
+                $this->dm->persist($element);
+                $this->dm->flush();
             }
         }
-        else {
-            // Feature does not exist on OSM
+        catch(\Exception $e) {
+            // Error when sending changeset
         }
     }
 
@@ -509,7 +465,7 @@ class ElementSynchronizationService
         $str = '';
 
         foreach($tags as $k => $v) {
-            if(strlen($str) > 0) { $str .= ', '; }
+            if (strlen($str) > 0) { $str .= ', '; }
             $str .= $k.' = '.$v;
         }
 
@@ -522,7 +478,7 @@ class ElementSynchronizationService
     private function allowOsmUpload($contribution, $preparedData) {
         return $contribution->hasBeenAccepted()
             && ($preparedData['action'] == 'edit' 
-            || $preparedData['action'] == 'add' && !$contribution->getElement()->getProperty('osm/version')); // when adding an element, if we find a duplicate on OSM and we say "that's the same", then the gogocarto element is merged with the OSM feature. But an Add contribution is still created, and we should ignore it
+            || $preparedData['action'] == 'add' && !$contribution->getElement()->getProperty('osm_version')); // when adding an element, if we find a duplicate on OSM and we say "that's the same", then the gogocarto element is merged with the OSM feature. But an Add contribution is still created, and we should ignore it
     }
 
     /**
diff --git a/src/Utils.php b/src/Utils.php
index 57c2dd19000bb0c697588bc6e48a928ab8d4dc12..7a02e8498de6ec1614c07f804f3fadf14c221cae 100755
--- a/src/Utils.php
+++ b/src/Utils.php
@@ -33,7 +33,7 @@ function slugify($text, $lowercase = true) {
     $text = str_replace('â', 'a', $text);
     $text = str_replace('î', 'i', $text);
     $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); // transliterate
-    $text = preg_replace('~[^\/\pL\d]+~u', '_', $text); // replace non letter by _
+    $text = preg_replace('~[^\pL\d]+~u', '_', $text); // replace non letter by _
     $text = trim($text, '_'); // trim
     $text = preg_replace('~_+~', '_', $text); // remove duplicate -
 
diff --git a/templates/admin/core_custom/custom-fields/element-address.html.twig b/templates/admin/core_custom/custom-fields/element-address.html.twig
index f6c9b51cca4a92e86c4cf48bd62a2c1d2ec8f02f..3bf51135486c211fd56734c5972246993d978de9 100755
--- a/templates/admin/core_custom/custom-fields/element-address.html.twig
+++ b/templates/admin/core_custom/custom-fields/element-address.html.twig
@@ -41,7 +41,7 @@
 	  {# var editMode = true; #}
     var defaultBounds = {{ config.getDefaultBounds|json_encode|raw }};
     var defaultTileLayer = "{{ config.defaultTileLayer.url }}";
-	  initMap();
+	  setTimeout(function() { initMap(); }, 0);
 
     $('.geocode-btn').on('click', function() {
       var address = "";
diff --git a/templates/admin/core_custom/custom-fields/element-data.html.twig b/templates/admin/core_custom/custom-fields/element-data.html.twig
index f1bef9426acc8799e62fb67503283bb8647f2410..75d5dbb1c63ec7403f054f3f3ecfa8d59b871df2 100755
--- a/templates/admin/core_custom/custom-fields/element-data.html.twig
+++ b/templates/admin/core_custom/custom-fields/element-data.html.twig
@@ -28,7 +28,7 @@
             {% set readonly = true %}
             {% set value = value|json_encode %}
           {% endif %}
-          {% if key matches '/^osm\\//' %}
+          {% if key matches '/^osm_/' %}
             {% set readonly = true %}
           {% endif %}
           <input type="{{ type }}" class="form-control" {{ readonly ? 'readonly="readonly"' : '' }}
diff --git a/templates/admin/core_custom/custom-fields/form-builder.html.twig b/templates/admin/core_custom/custom-fields/form-builder.html.twig
index b6a52e4466a95d1839f8e332204da945df62952a..f6c1b42998415521d0dbae4171d59b49675e75bc 100755
--- a/templates/admin/core_custom/custom-fields/form-builder.html.twig
+++ b/templates/admin/core_custom/custom-fields/form-builder.html.twig
@@ -40,13 +40,16 @@
         elements: function(fieldData) { return { field: '<select id="' + fieldData.name + '"><option>'+ fieldData.label+'</option></select>' }; },
       };
 
-      var iconAttr = { label: 'Icone', placeholder: 'Choisissez une icone'  } // TODO Translation
-      var errorMsgAttrs = { label: "Msg. Erreur", placeholder: "Oups ce texte est un peu long ! // Veuillez renseigner une adresse email valide // ..." } // TODO Translation
-      var searchAttrs = { label: 'Recherche dans ce champ', type: 'checkbox' }; // TODO Translation
-      var searchWeightAttrs = { label: 'Poids de la recherche', type: 'number', value: "1" }; // TODO Translation
+      var iconAttr = { label: 'Icone', placeholder: 'Choisissez une icone'  }
+      var labelAttr = { label: 'Label' }
+      var errorMsgAttrs = { label: "Msg. Erreur", placeholder: "Oups ce texte est un peu long ! // Veuillez renseigner une adresse email valide // ..." }
+      var searchAttrs = { label: 'Recherche dans ce champ', type: 'checkbox' };
+      var searchWeightAttrs = { label: 'Poids de la recherche', type: 'number', value: "1" };
+      var patternAttrs = { label: 'Pattern de validation', placeholder: "Expression régulière pour la validation de ce champ" };
       var typeUserAttrs = {
         text: {
           icon: iconAttr,
+          label: labelAttr,
           separator: { label: '' }, // separate important attrs from others
           subtype: { label: 'Type', options: {
               'text': 'Texte', // TODO Translation
@@ -57,10 +60,12 @@
           },
           search: searchAttrs,
           searchWeight: searchWeightAttrs,
+          pattern: patternAttrs,
           errorMsg: errorMsgAttrs
         },
         textarea: {
           icon: iconAttr,
+          label: labelAttr,
           subtype: { label: 'Type', options: {
               'textarea': 'Editeur simple', // TODO Translation
               'wysiwyg': 'Editeur enrichi', // TODO Translation
@@ -71,54 +76,72 @@
           errorMsg: errorMsgAttrs,
           separator: { label: '' }, // separate important attrs from others
         },
-        select: { icon: iconAttr, errorMsg: errorMsgAttrs },
-        number: { icon: iconAttr, errorMsg: errorMsgAttrs },
+        paragraph: { 
+          label: {
+            label: "Contenu",
+            type: "textarea"
+          }
+        },
+        header: { label: labelAttr },
+        select: { icon: iconAttr, label: labelAttr, errorMsg: errorMsgAttrs },
+        number: { icon: iconAttr, label: labelAttr, errorMsg: errorMsgAttrs },
         title: {
-          maxlength: { label: "Longueur Max."}, // TODO Translation
+          label: labelAttr,
+          maxlength: { label: "Longueur Max."},
           icon: iconAttr,
           search: searchAttrs,
           searchWeight: searchWeightAttrs,
           errorMsg: errorMsgAttrs,
           separator: { label: '' }, // separate important attrs from others
         },
-        address: { icon: iconAttr },
+        address: { icon: iconAttr, label: labelAttr },
         'checkbox-group': {
-          style: { label: 'Style des cases', options: { // TODO Translation
-            'normal': 'Normal', // TODO Translation
-            'filled': 'Plein', // TODO Translation
-          }, errorMsg: errorMsgAttrs }
+          style: { 
+            label: 'Style des cases', 
+            options: { 'normal': 'Normal', 'filled': 'Plein' }
+          }, 
+          errorMsg: errorMsgAttrs, 
+          label: labelAttr
         },
         checkbox: {
-          defaultvalue: { label: 'Valeur initiale', options: {
-            'no': 'Non cochée', // TODO Translation
-            'yes': 'Cochée', // TODO Translation
-          }, errorMsg: errorMsgAttrs }
+          label: labelAttr,
+          defaultvalue: { 
+            label: 'Valeur initiale', 
+            options: { 'no': 'Non cochée', 'yes': 'Cochée' }
+          },
+          errorMsg: errorMsgAttrs
         },
         email: {
           icon: iconAttr,
+          label: labelAttr,
           errorMsg: errorMsgAttrs,
           separator: { label: '' }, // separate important attrs from others
         },
         image: {
           icon: iconAttr,
+          label: labelAttr,
           errorMsg: errorMsgAttrs,
           separator: { label: '' }, // separate important attrs from others
         },
         images: {
           icon: iconAttr,
+          label: labelAttr,
           separator: { label: '' }, // separate important attrs from others
         },
         files: {
           icon: iconAttr,
-          accept: { label: "Fichier acceptés", placeholder: ".pdf audio/* .mp3 (séparés par des espaces)"}, // TODO Translation
+          label: labelAttr,
+          accept: { label: "Fichier acceptés", placeholder: ".pdf audio/* .mp3 (séparés par des espaces)"},
           separator: { label: '' }, // separate important attrs from others
         },
         date: {
           icon: iconAttr,
-          timepicker: { label: "Timepicker", type: 'checkbox'}, // TODO Translation
-          range: { label: "Intervale de dates", type: 'checkbox' }, // TODO Translation
+          label: labelAttr,
+          timepicker: { label: "Timepicker", type: 'checkbox'},
+          range: { label: "Intervale de dates", type: 'checkbox' },
         },
         elements: {
+          label: labelAttr,
           icon: iconAttr,
           reversedBy: { label: 'Inversée par', placeholder: "Le nom d'un autre champ de type \"lien vers un autre élément\"" }, // TODO Translation
           multiple: { label: 'Plusieurs choix', type: 'checkbox' }, // TODO Translation
@@ -160,6 +183,15 @@
         $('.input-control[data-type=files]').toggle($('.files-field').length == 0);
         $('.input-control[data-type=openhours]').toggle($('.openhours-field').length == 0);
 
+        $('input[type="textarea"]').replaceWith(function() {
+          const textarea = document.createElement('textarea')
+          textarea.id = this.id
+          textarea.name = this.name
+          textarea.value = this.value
+          textarea.classList = this.classList
+          textarea.title = this.title
+          return textarea
+        })
         // $('.name-wrap input[name=name]').val('email');
         // get all input names (used to check for uniqueness)
         var allNames = [];
@@ -300,4 +332,13 @@
   /* Make select2 looks like other inputs */
   .form-wrap.form-builder .form-control.select2-container { padding: 0; }
   .form-wrap.form-builder .form-control:not(.select2-dropdown-open) .select2-choice { border-radius: 5px !important; }
+
+  /* fix menu sticky position */
+  .form-wrap.form-builder .cb-wrap {
+    top: 100px !important;
+  }
+  /* Fix menu action soemtime not clickable */
+  .form-wrap.form-builder .frmb .field-actions {
+    z-index: 500000;
+  }
 </style>
\ No newline at end of file
diff --git a/templates/admin/core_custom/custom-fields/mapping-ontology.html.twig b/templates/admin/core_custom/custom-fields/mapping-ontology.html.twig
index de0bd6dc8044d480e41f92ce470b74aa04c7fc6f..2afca7209d847ce23f59027bd78e26ca0bcc6957 100755
--- a/templates/admin/core_custom/custom-fields/mapping-ontology.html.twig
+++ b/templates/admin/core_custom/custom-fields/mapping-ontology.html.twig
@@ -127,10 +127,11 @@
       specificData.push({id: 'source', text: "Origine de l'élément (source)"});    // TODO Translation 
     specificData.push({id: 'openHours', text: "Horaires d'ouverture (format GoGoCarto)"}); // TODO Translation
     if (importType == 'osm') {
-      specificData.push({id: 'osm/source', text: "Source de la donnée OpenStreetMap"}); // TODO Translation
-      specificData.push({id: 'osm/opening_hours', text: "Horaires d'ouverture (format OSM)"}); // TODO Translation
-      specificData.push({id: 'osm/version', text: "OSM Version (mappé automatiquement)"}); // TODO Translation
-      specificData.push({id: 'osm/timestamp', text: "OSM Timestamp (mappé automatiquement)"}); // TODO Translation
+      specificData.push({id: 'osm_source', text: "Source de la donnée OpenStreetMap"});
+      specificData.push({id: 'osm_opening_hours', text: "Horaires d'ouverture (format OSM)"});
+      specificData.push({id: 'osm_version', text: "OSM Version (mappé automatiquement)"});
+      specificData.push({id: 'osm_timestamp', text: "OSM Timestamp (mappé automatiquement)"});
+      specificData.push({id: 'osm_url', text: "OSM Url (mappé automatiquement)"});
     }    
 
     allProperties = $.map(coreData.concat(specificData), function(el) { return el.id });
@@ -155,6 +156,7 @@
       var originName = '{{ originName }}';
       if (originName && allProperties.indexOf(originName) == -1 && coreFields.indexOf(originName) == -1 && originName != '/')
       {
+        originName = originName.split('/')[originName.split('/').length - 1]
         importedData.push({id: originName, text:  originName});
         allProperties.push(originName);
       }
@@ -203,7 +205,9 @@
 
     // on arrow click, use the original name
     $('.mapping-ontology-table .arrow-icon').click(function() {
-      $(this).parent().parent().find('.property-selector:not(.select2-container)').val($(this).parent().siblings('.original').text()).trigger('change');
+      let valueTocopy = $(this).parent().siblings('.original').text().split('/')
+      valueTocopy = valueTocopy[valueTocopy.length - 1] // get last
+      $(this).parent().parent().find('.property-selector:not(.select2-container)').val(valueTocopy).trigger('change');
     });
 
     $('.mapping-ontology-table .clear-icon').click(function() {
diff --git a/templates/base-layout.html.twig b/templates/base-layout.html.twig
index 44f53a5b1cd16115e4c7473860cbc3a55b0978f2..efd3f5b51824e581a55d5d077ee5d88a5a580452 100755
--- a/templates/base-layout.html.twig
+++ b/templates/base-layout.html.twig
@@ -1,8 +1,10 @@
 <!DOCTYPE html>
 
+{% set config = helper.config %}
+
 <html class="no-js gogo-load-css" lang="fr">
   <head>
-    <title>{% block title %}GoGoCarto{# config.appName TODO translation config not always defined #}{% endblock %}</title>
+    <title>{% block title %}config.appName{% endblock %}</title>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 
     {# gogocarto contains materializeCss lib, so we load it for all pages #}
@@ -16,7 +18,7 @@
     <meta name="robots" content="index, follow, all" />
   </head>
 
-  <body class="{% if config is defined %}gogo-theme-{{ config.theme }}{% endif %} {% if pageName is defined %}{{ pageName }}{% endif %} base-layout"> {# use this class to load the css vendors encapsulated inside gogocarto.css #}
+  <body class="gogo-theme-{{ config.theme }} {% if pageName is defined %}{{ pageName }}{% endif %} base-layout"> {# use this class to load the css vendors encapsulated inside gogocarto.css #}
 
     {# Header with navigation links, login button... #}
     {% if not hideHeader is defined %}
@@ -28,6 +30,8 @@
 
     {# Mains javascripts libs #}
     <script src="{{ asset('js/gogocarto.js?ver=' ~ version) }}"></script>
+
+    <script>gogoLocale = "{{ config.locale }}"</script>
     
     {# Block to add custom javascript to each page #}
     {% block javascripts %}
diff --git a/templates/bundles/SonataAdminBundle/layout.html.twig b/templates/bundles/SonataAdminBundle/layout.html.twig
index 47d52fa652776e28c19bf38536ed8dd73089011a..a3441e0d0a28d094e8c2ed36fc80ca04a9ad9b1f 100755
--- a/templates/bundles/SonataAdminBundle/layout.html.twig
+++ b/templates/bundles/SonataAdminBundle/layout.html.twig
@@ -25,6 +25,8 @@
     <script src="{{ asset('bundles/sonataformatter/markitup/sets/html/set.js') }}" type="text/javascript"></script>
     <script src="{{ asset('bundles/sonataformatter/markitup/sets/textile/set.js') }}" type="text/javascript"></script>
 
+    <script>gogoLocale = "{{ helper.config.locale }}"</script>
+
     <script>
     // bootstrap-ckeditor-modal-fix.js
     // hack to fix ckeditor/bootstrap compatiability bug when ckeditor appears in a bootstrap modal dialog
diff --git a/templates/element-form/form-partials/select.html.twig b/templates/element-form/form-partials/select.html.twig
index b7ca248557147911ccbae3fc9621b24ad9774407..1974e565d1bbc5addb333bac009222c2ef02251d 100755
--- a/templates/element-form/form-partials/select.html.twig
+++ b/templates/element-form/form-partials/select.html.twig
@@ -6,11 +6,15 @@
   <select name="data[{{ field.name }}]" {% if field.multiple is defined %}multiple{% endif %}>
     {% if field.placeholder is defined %}
       <option value="" disabled selected>{{ field.placeholder|raw }}</option>
+    {% else %}
+      {# Use label as placeholder, so the select input looks the same than the text input #}
+      <option value="" disabled selected class="to-html">{{ field.label|raw }}</option>
     {% endif %}
 
     {% for option in field.values %}
       <option value="{{ option.value }}"
-        {% if elementValue == option.value or not elementValue and option.selected is defined and not field.placeholder is defined %}selected{% endif %}>{{ option.label|raw }}</option>
+        {# or not elementValue and option.selected is defined and not field.placeholder is defined #}
+        {% if elementValue == option.value %}selected{% endif %}>{{ option.label|raw }}</option>
     {% endfor %}
   </select>
 
diff --git a/templates/element-form/form-partials/taxonomy-field.html.twig b/templates/element-form/form-partials/taxonomy-field.html.twig
index 10b17270c69538814e15cd395b9f6c57081c3fbb..49e0f64ae38981991511ed02ab84994668ec505e 100755
--- a/templates/element-form/form-partials/taxonomy-field.html.twig
+++ b/templates/element-form/form-partials/taxonomy-field.html.twig
@@ -43,9 +43,9 @@
   			{% endif %}
   		{% endfor %}
 
-  		<div class="option-field depth-{{ categoryDepth }} {{ displayInline ? 'inline' : ''}}" id="option-field-{{option.id}}"
-            {% if not displayOption %} style="display:none"{% endif %}
-  					data-id="{{option.id}}" data-index={{curr_index}}>
+  		<div class="option-field depth-{{ categoryDepth }} {{ displayInline ? 'inline' : ''}} {{ displayOption ? 'selected' : '' }}" 
+		     id="option-field-{{option.id}}"
+  			 data-id="{{option.id}}" data-index={{curr_index}}>
 
   			<div class="option-field-value {{ category.enableDescription ? "with-description" : "" }}">
 
@@ -60,7 +60,7 @@
 
   				{% if category.enableDescription %}
     				<div class="input-field option-field-description">
-  				  	<input placeholder="{{ 'commons.category_placeholder'|trans }}" id="option-fields-description-{{ option.id }}" class="option-field-description-input" data-id="{{option.id}}" type="text" length="80" value="{{curr_description}}">
+  				  		<input placeholder="{{ category.descriptionLabel|default('commons.category_placeholder'|trans) }}" id="option-fields-description-{{ option.id }}" class="option-field-description-input" data-id="{{option.id}}" type="text" length="80" value="{{curr_description}}">
     				</div>
   				{% endif %}
 
diff --git a/templates/element-form/form-partials/taxonomy.html.twig b/templates/element-form/form-partials/taxonomy.html.twig
index 3fbad1a1abb5c7c5a0f9f354aacaeb70cf004a15..0752c76a2af16625e1e7a1a11c4f6d2146a9a6f1 100755
--- a/templates/element-form/form-partials/taxonomy.html.twig
+++ b/templates/element-form/form-partials/taxonomy.html.twig
@@ -1,4 +1,6 @@
-<div class="categories-info neutral-color to-html">{{ field.label|raw }}</div>
+{% if field.label is defined and field.label != "undefined" %}
+  <div class="categories-info neutral-color to-html">{{ field.label|raw }}</div>
+{% endif %}
 {% for mainCategory in mainCategories %}
   {% include 'element-form/form-partials/taxonomy-field.html.twig' with { 'category' : mainCategory } %}
 {% endfor %}
diff --git a/templates/element-form/form-partials/text.html.twig b/templates/element-form/form-partials/text.html.twig
index 96eaa0dc73d2274c09a90a765b48ec07371eb451..1ac8f920607b6eeb6a2cf06c9245a6482bc5ed34 100755
--- a/templates/element-form/form-partials/text.html.twig
+++ b/templates/element-form/form-partials/text.html.twig
@@ -20,7 +20,7 @@
   {% endif %}
 
   {# ERROR MESSAGE #}
-  {% set errorMsg = fields.errorMsg|default(null) %}
+  {% set errorMsg = field.errorMsg|default(null) %}
   {% if not errorMsg %}
     {% if field.subtype is defined and field.subtype == "email" or field.type == 'email' %}
       {% set errorMsg = 'commons.errors.bad_email'|trans %}
@@ -71,6 +71,7 @@
       {% if field.rows is defined %} rows="{{ field.rows }}"{% endif %}
       {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}"{% endif %}
       {% if field.maxlength is defined %} length="{{ field.maxlength }}"{% endif %}
+      {% if field.pattern is defined %} pattern="{{ field.pattern }}"{% endif %}
   {% if field.type == "textarea" %}
     ></textarea>
   {% else %}
diff --git a/templates/element-form/form.html.twig b/templates/element-form/form.html.twig
index 596fe9711a595fad408b5fd6bed6e7de7d1a98bc..edb3166560535669dc528da9a78e1664c15d8166 100755
--- a/templates/element-form/form.html.twig
+++ b/templates/element-form/form.html.twig
@@ -1,6 +1,6 @@
 {{ form_start(form, {'id': 'formElement'}) }}
 
-<div id="element-form-content">
+<div id="element-form-content" class="{{ editMode ? 'edit' : 'new' }} {{ isOwner ? 'by-owner' : ''}} {{ isAllowedDirectModeration ? 'by-admin' : '' }}">
 	<section>
 		{% include "element-form/form-renderer.html.twig" %}
 	</section>
diff --git a/templates/partners.html.twig b/templates/partners.html.twig
index 32c9aee6de078642c1eec74788504c3ac0b048fd..ac31d3599a7ee9b3f92cd3ef89263ecab98a92d6 100755
--- a/templates/partners.html.twig
+++ b/templates/partners.html.twig
@@ -12,28 +12,27 @@
 
 <h1>{{ config.partnerPageTitle }}</h1>
 
-{% for partner in listPartners %}
-
-  <div class="partner-item row">
-    {% set hasImage = partner.logo and partner.logo.imageUrl %}
-    {% if hasImage %}
-      <div class="image-container">
-        <img src="{{ partner.logo.imageUrl }}" alt="logo" class="partner-logo"/>
-      </div>
-    {% endif %}
-    <div class="partner-text">
-      <h2 class="partner-title {{ hasImage ? "with-logo" : 'no-logo' }}">{{ partner.name|capitalize }}</h2>
-      <div class="partner-description wysiwyg-content">{{ partner.content | raw }} </div>
-      {% if partner.websiteUrl %}
-        {% set mailto = partner.websiteUrl|split('mailto') %}
-        {% set isMail = (mailto|length > 1) %}
-        <a class="partner-url" href="{{ partner.websiteUrl }}" {% if not isMail %}target="_blank"{% endif %}>{{ partner.websiteUrl | replace({'https://': "", 'http://': "", 'mailto:':""}) }}</a>
-
+<div class="partners-container">
+  {% for partner in listPartners %}
+    {% set mailto = partner.websiteUrl|default('')|split('mailto') %}
+    {% set isMail = (mailto|length > 1) %}
+    <a class="partner-item row" href="{{ partner.websiteUrl }}" {% if not isMail %}target="_blank"{% endif %}>
+      {% set hasImage = partner.logo and partner.logo.imageUrl %}
+      {% if hasImage %}
+        <div class="image-container">
+          <img src="{{ partner.logo.imageUrl }}" alt="logo" class="partner-logo"/>
+        </div>
       {% endif %}
-    </div>
-  </div>
-
-{% endfor %}
+      <div class="partner-text">
+        <h2 class="partner-title {{ hasImage ? "with-logo" : 'no-logo' }}">{{ partner.name|capitalize }}</h2>
+        <div class="partner-description wysiwyg-content">{{ partner.content | raw }} </div>
+        {% if partner.websiteUrl %}          
+          <a class="partner-url" href="{{ partner.websiteUrl }}" {% if not isMail %}target="_blank"{% endif %}>{{ partner.websiteUrl | replace({'https://': "", 'http://': "", 'mailto:':""}) }}</a>
+        {% endif %}
+      </div>
+    </a>
+  {% endfor %}
+</div>
 
 </section>
 
diff --git a/templates/styles/form.html.twig b/templates/styles/form.html.twig
index cb1d542842788b0f7c231cf25becfd8d4a6a4805..04f5b66daf107d9302bf620968040f746d415483 100755
--- a/templates/styles/form.html.twig
+++ b/templates/styles/form.html.twig
@@ -36,15 +36,15 @@
   }
 
   {# Input #}
-  .gogo-load-css #page-content.element-form input:focus:not(.invalid):not([readonly]):not([type=submit]), .gogo-load-css textarea:focus:not(.invalid):not([readonly]) {
+  .gogo-load-css #page-content.element-form .field-container:not(.field-taxonomy) input:focus:not(.invalid):not([readonly]):not([type=submit]), .gogo-load-css textarea:focus:not(.invalid):not([readonly]) {
     border-color: {{ textSoft }} !important;
     box-shadow: 0 1px 0 0 {{ textSoft }} !important;
   }
-  .gogo-load-css #page-content.element-form input.invalid:not([readonly]):not([type=submit]), .gogo-load-css textarea.invalid:not([readonly]) {
+  .gogo-load-css #page-content.element-form .field-container:not(.field-taxonomy) input.invalid:not([readonly]):not([type=submit]), .gogo-load-css textarea.invalid:not([readonly]) {
     border-color: {{ error }} !important;
     box-shadow: 0 1px 0 0 {{ error }} !important;
   }
-  .gogo-load-css #page-content.element-form input:not(:focus):not(.invalid):not([type=submit]), .gogo-load-css textarea:not(:focus):not(.invalid) {
+  .gogo-load-css #page-content.element-form .field-container:not(.field-taxonomy) input:not(:focus):not(.invalid):not([type=submit]), .gogo-load-css textarea:not(:focus):not(.invalid) {
     border-bottom: 1px solid {{ disable }} !important;
     box-shadow: none !important;
   }
@@ -60,6 +60,7 @@
   {# Input Label #}
   .gogo-load-css #page-content.element-form label { color: {{ textSoft }}; }
   .gogo-load-css label.gogo-form-label { color: {{ disable }} !important }
+  .gogo-load-css .select-input:not(.initialized) { color: {{ disable }} !important }
   .gogo-load-css input:focus:not(.invalid) + label.gogo-form-label, .gogo-load-css textarea:focus:not(.invalid) + label.gogo-form-label { color: {{ text }} !important; }
   .gogo-load-css input.invalid + label.gogo-form-label, .gogo-load-css textarea.invalid + label.gogo-form-label { color: {{ error }} !important; }
   .gogo-load-css input[type=date].invalid+label:after, .gogo-load-css input[type=date]:focus.invalid+label:after, .gogo-load-css input[type=datetime-local].invalid+label:after, .gogo-load-css input[type=datetime-local]:focus.invalid+label:after, .gogo-load-css input[type=email].invalid+label:after, .gogo-load-css input[type=email]:focus.invalid+label:after, .gogo-load-css input[type=number].invalid+label:after, .gogo-load-css input[type=number]:focus.invalid+label:after, .gogo-load-css input[type=password].invalid+label:after, .gogo-load-css input[type=password]:focus.invalid+label:after, .gogo-load-css input[type=search].invalid+label:after, .gogo-load-css input[type=search]:focus.invalid+label:after, .gogo-load-css input[type=tel].invalid+label:after, .gogo-load-css input[type=tel]:focus.invalid+label:after, .gogo-load-css input[type=text].invalid+label:after, .gogo-load-css input[type=text]:focus.invalid+label:after, .gogo-load-css input[type=time].invalid+label:after, .gogo-load-css input[type=time]:focus.invalid+label:after, .gogo-load-css input[type=url].invalid+label:after, .gogo-load-css input[type=url]:focus.invalid+label:after, .gogo-load-css textarea.materialize-textarea.invalid+label:after, .gogo-load-css textarea.materialize-textarea:focus.invalid+label:after {
diff --git a/translations/admin+intl-icu.fr.yaml b/translations/admin+intl-icu.fr.yaml
index 342f1be05d80f55aaef3f24b5a78c6e591dcad6f..663ba9404f265d9cd5c814a9ef45abc388f67ec0 100755
--- a/translations/admin+intl-icu.fr.yaml
+++ b/translations/admin+intl-icu.fr.yaml
@@ -500,6 +500,7 @@ categories:
     singleOption_help: "Une seule catégorie est selectionnable à la fois"
     enableDescription: "Activer la description des catégories"
     enableDescription_help: "On pourra renseigner un texte pour décrire chaque catégorie. Par example, pour un catégorie Agriculture, on pourrait ajouter comme texte \"Maraîchage, produits transformés...\""
+    descriptionLabel: Label pour le champ de description
     nameShort: "Nom (version courte)"
     nameShort_help: "La version courte est utilisée dans le menu, car souvent on manque de place"
     index: "Position"
diff --git a/translations/javascripts-translations.yaml b/translations/javascripts-translations.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1be1490ccd3ac4d7e431ac11503b7eb89e9a5536
--- /dev/null
+++ b/translations/javascripts-translations.yaml
@@ -0,0 +1,3 @@
+fr:
+  element-form:
+    geocoded-marker-text: Déplacez moi pour préciser la position</br>(au centre du bâtiment)
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 2b8f4830d567410d33c9b06b96eebe9cdf6eb653..f97274b428138aac22358ac8c8f2feeadf451210 100755
--- a/yarn.lock
+++ b/yarn.lock
@@ -885,6 +885,11 @@
   resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
   integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
 
+"@types/geojson@*":
+  version "7946.0.7"
+  resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
+  integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==
+
 "@types/glob@^7.1.1":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
@@ -1966,6 +1971,13 @@ bufferstreams@1.0.1:
   dependencies:
     readable-stream "^1.0.33"
 
+bufferstreams@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/bufferstreams/-/bufferstreams-2.0.1.tgz#441b267c2fc3fee02bb1d929289da113903bd5ef"
+  integrity sha512-ZswyIoBfFb3cVDsnZLLj2IDJ/0ppYdil/v2EGlZXvoefO689FokEmFEldhN5dV7R2QBxFneqTJOMIpfqhj+n0g==
+  dependencies:
+    readable-stream "^2.3.6"
+
 builtin-status-codes@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -2461,7 +2473,7 @@ concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@
     readable-stream "^2.2.2"
     typedarray "^0.0.6"
 
-concat-with-sourcemaps@^1.0.0:
+concat-with-sourcemaps@^1.0.0, concat-with-sourcemaps@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e"
   integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==
@@ -4112,10 +4124,10 @@ glogg@^1.0.0:
   dependencies:
     sparkles "^1.0.0"
 
-gogocarto-js@^1.7.2:
-  version "1.7.2"
-  resolved "https://registry.yarnpkg.com/gogocarto-js/-/gogocarto-js-1.7.2.tgz#85aac85d9abebdbd70120357fd05eeba088cf419"
-  integrity sha512-VtaGtYxhZe6duWasWOZCX9SZhF+Xz3WQK7HK4/WK5zpK0lyuC9NgV0xqwZctNYd4d3Mc0z3PtASrq2VRQDlmCg==
+gogocarto-js@^1.7.3:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/gogocarto-js/-/gogocarto-js-1.7.3.tgz#c79f430d422b089e5a4bca154529a5cc2bba0424"
+  integrity sha512-lBgCNymbkUopiLgB4POk4W90ZoL679ImVA9J9kgA/0wjgovau0i94dopBxGRdFbxVzAglSlKdxg/4rea48NFHQ==
   dependencies:
     commonmark "^0.29.0"
     diff "^4.0.1"
@@ -4126,7 +4138,7 @@ gogocarto-js@^1.7.2:
     nouislider "^14.0.3"
     nunjucks "^3.2.0"
     tinycolor2 "^1.4.1"
-    universal-geocoder "^0.3.0"
+    universal-geocoder "^0.13.0"
     unorm "^1.6.0"
 
 graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
@@ -4189,6 +4201,16 @@ gulp-gzip@^1.4.0:
     stream-to-array "^2.3.0"
     through2 "^2.0.3"
 
+gulp-header@^2.0.9:
+  version "2.0.9"
+  resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-2.0.9.tgz#8b432c4d4379dee6788845b16785b09c7675af84"
+  integrity sha512-LMGiBx+qH8giwrOuuZXSGvswcIUh0OiioNkUpLhNyvaC6/Ga8X6cfAeme2L5PqsbXMhL8o8b/OmVqIQdxprhcQ==
+  dependencies:
+    concat-with-sourcemaps "^1.1.0"
+    lodash.template "^4.5.0"
+    map-stream "0.0.7"
+    through2 "^2.0.0"
+
 gulp-minify-css@^1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/gulp-minify-css/-/gulp-minify-css-1.2.4.tgz#b6164957602ea27f9e5ad88227695dd205778c06"
@@ -4253,6 +4275,18 @@ gulp-util@^3.0.5:
     through2 "^2.0.0"
     vinyl "^0.5.0"
 
+gulp-yaml@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/gulp-yaml/-/gulp-yaml-2.0.4.tgz#86569e2becc9f5dfc95dc92db5a71a237f4b6ab4"
+  integrity sha512-S/9Ib8PO+jGkCvWDwBUkmFkeW7QM0pp4PO8NNrMEfWo5Sk30P+KqpyXc4055L/vOX326T/b9MhM4nw5EenyX9g==
+  dependencies:
+    bufferstreams "^2.0.1"
+    js-yaml "^3.13.1"
+    object-assign "^4.1.1"
+    plugin-error "^1.0.1"
+    replace-ext "^1.0.0"
+    through2 "^3.0.0"
+
 gulp@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa"
@@ -5476,7 +5510,7 @@ lodash.template@^3.0.0:
     lodash.restparam "^3.0.0"
     lodash.templatesettings "^3.0.0"
 
-lodash.template@^4.4.0:
+lodash.template@^4.4.0, lodash.template@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
   integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
@@ -5593,6 +5627,11 @@ map-obj@^1.0.0, map-obj@^1.0.1:
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
   integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
 
+map-stream@0.0.7:
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8"
+  integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=
+
 map-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -7172,6 +7211,15 @@ read-pkg@^1.0.0:
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
+"readable-stream@2 || 3", readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
 readable-stream@^1.0.33, readable-stream@~1.1.9:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -7182,15 +7230,6 @@ readable-stream@^1.0.33, readable-stream@~1.1.9:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
-  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
-  dependencies:
-    inherits "^2.0.3"
-    string_decoder "^1.1.1"
-    util-deprecate "^1.0.1"
-
 readdirp@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -8381,6 +8420,14 @@ through2@^2.0.0, through2@^2.0.3, through2@~2.0.0:
     readable-stream "~2.3.6"
     xtend "~4.0.1"
 
+through2@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4"
+  integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==
+  dependencies:
+    inherits "^2.0.4"
+    readable-stream "2 || 3"
+
 "through@>=2.2.7 <3":
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -8686,11 +8733,12 @@ unique-stream@^2.0.2:
     json-stable-stringify-without-jsonify "^1.0.1"
     through2-filter "^3.0.0"
 
-universal-geocoder@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/universal-geocoder/-/universal-geocoder-0.3.0.tgz#4d5e061734b44360fbfc2fcce6320810c0951756"
-  integrity sha512-WeNHbhZgTtzLKVsI//rGIVQWxFxFwRTRn8Q/VM6D1WxJniXkvVRFqWao9/o3z8AuEg698shJjfdY5+v3nc1zyQ==
+universal-geocoder@^0.13.0:
+  version "0.13.0"
+  resolved "https://registry.yarnpkg.com/universal-geocoder/-/universal-geocoder-0.13.0.tgz#e8612f91b066babb2b8bd5766209c789d3e4f4a6"
+  integrity sha512-VK/oEoRjtPSnHHMoINW+rVXB4n2ddJ0mnv0rwYjYAp7jIhilZNRjnCEwUoH7OZkNXMnbeqzGnSDV5x4b32JMQw==
   dependencies:
+    "@types/geojson" "*"
     cross-fetch "^3.0.0"
 
 universal-geocoder@^0.5.0: