Merge pull request #9603 from openstreetmap/addr_place

Support addr:place tag in Address field
This commit is contained in:
Martin Raifer
2023-05-26 20:47:35 +02:00
committed by GitHub
5 changed files with 211 additions and 91 deletions
+3 -1
View File
@@ -41,6 +41,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
#### :sparkles: Usability & Accessibility
* Make it easier to search for OSM objects by id ([#9520], thanks [@k-yle])
* Localize numbers in numeric fields ([#8769], thanks [@1ec5])
* The Address field now supports the `addr:place` tag (as an alternative to `addr:street`), this functionality is activated in selected countries ([#9603])
#### :scissors: Operations
#### :camera: Street-Level
#### :white_check_mark: Validation
@@ -65,7 +66,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
* Render "right-side" arrows for features with lifecycle prefixes ([#9493], thanks [@k-yle])
* Take regional variants of parent presets into account when resolving preset fields ([#9524])
* Render "right-side" arrows for `man_made=quay` features
* Add support icons also in `multiCombo` and `semiCombo` fields ([#9433])
* Support icons also in `multiCombo` and `semiCombo` fields ([#9433])
#### :hammer: Development
* Upgrade dependencies: `fortawesome` to v6.4, `which-polygon` to v2.2.1, `glob` to v9.2, `temaki` to v5.4, `marked` to v4.3, `core-js-bundle` to v3.30, `osm-auth` to v2.1
* Bundle `package-lock.json` file in repository for faster `clean-install` builds
@@ -82,6 +83,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
[#9501]: https://github.com/openstreetmap/iD/pull/9501
[#9520]: https://github.com/openstreetmap/iD/pull/9520
[#9524]: https://github.com/openstreetmap/iD/issues/9524
[#9603]: https://github.com/openstreetmap/iD/pull/9603
[#9630]: https://github.com/openstreetmap/iD/pull/9630
[#9637]: https://github.com/openstreetmap/iD/pull/9637
[#9638]: https://github.com/openstreetmap/iD/pull/9638
+13
View File
@@ -2128,6 +2128,19 @@ input.date-selector {
.ideditor[dir='rtl'] .addr-row:last-of-type input:last-of-type {
border-radius: 0 0 0 4px;
}
.combobox-address-street-place .combobox-option.address-street,
.combobox-address-street-place .combobox-option.address-place {
padding-right: 20px;
}
.combobox-address-street-place .combobox-option.address-street::after,
.combobox-address-street-place .combobox-option.address-place::after {
position: absolute;
right: 2px;
opacity: 0.4;
}
.combobox-address-street-place .combobox-option.address-place::after {
content: url(data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMCIgeT0iMCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiB2aWV3Qm94PSIwIDAgMjAgMjAiPg0KICAgIDxwYXRoIGQ9Ik0xMCwzIEM2LjY4NiwzIDQsNS42MTIgNCw4LjgzMyBDNCwxMi4wNTUgMTAsMTcgMTAsMTcgQzEwLDE3IDE2LDEyLjA1NSAxNiw4LjgzMyBDMTYsNS42MTIgMTMuMzE0LDMgMTAsMyB6IE0xMC4xODcsNi41IEMxMS41MTMsNi41IDEyLjU4Nyw3LjU0NSAxMi41ODcsOC44MzMgQzEyLjU4NywxMC4xMjIgMTEuNTEzLDExLjE2NyAxMC4xODcsMTEuMTY3IEM4Ljg2MiwxMS4xNjcgNy43ODcsMTAuMTIyIDcuNzg3LDguODMzIEM3Ljc4Nyw3LjU0NSA4Ljg2Miw2LjUgMTAuMTg3LDYuNSB6IiBmaWxsPSJjdXJyZW50Q29sb3IiLz4NCjwvc3ZnPg==);
}
/* Field - Wikipedia
+48 -11
View File
@@ -1,7 +1,7 @@
[
{
"format": [
["housenumber", "street"],
["housenumber", "street+place"],
["city", "postcode"]
]
},
@@ -16,7 +16,7 @@
"countryCodes": ["gb"],
"format": [
["housename"],
["housenumber", "street"],
["housenumber", "street+place"],
["city", "postcode"]
]
},
@@ -24,22 +24,31 @@
"countryCodes": ["ie"],
"format": [
["housename"],
["housenumber", "street"],
["housenumber", "street+place"],
["city"],
["postcode"]
]
},
{
"countryCodes": ["at", "bg", "ch", "de", "si", "pl"],
"countryCodes": ["at", "bg", "ch", "de", "si", "pl", "lt"],
"format": [
["street", "housenumber"],
["street+place", "housenumber"],
["postcode", "city"]
]
},
{
"countryCodes": [
"ad", "ba", "be", "cz", "dk", "es", "fi", "gr", "hr", "is",
"it", "li", "nl", "no", "pt", "se", "sk", "sm", "va"
"ad", "ba", "be", "dk", "es", "gr", "hr",
"it", "pt", "se", "sm", "va"
],
"format": [
["street+place", "housenumber", "unit"],
["postcode", "city"]
]
},
{
"countryCodes": [
"fi", "is", "li", "nl", "no"
],
"format": [
["street", "housenumber", "unit"],
@@ -47,16 +56,30 @@
]
},
{
"countryCodes": ["fr", "lu", "mo"],
"countryCodes": ["fr", "lu"],
"format": [
["housenumber", "street+place"],
["postcode", "city"]
]
},
{
"countryCodes": ["mo"],
"format": [
["housenumber", "street"],
["postcode", "city"]
]
},
{
"countryCodes": ["lv"],
"format": [
["street", "housenumber"],
["city", "postcode"]
]
},
{
"countryCodes": ["br"],
"format": [
["street"],
["street+place"],
["housenumber", "suburb"],
["city", "postcode"]
]
@@ -89,7 +112,7 @@
"countryCodes": ["tw"],
"format": [
["postcode", "city", "district"],
["place", "street"],
["street+place"],
["housenumber", "floor", "unit"]
]
},
@@ -121,7 +144,7 @@
"countryCodes": ["tr"],
"format": [
["neighbourhood"],
["street", "housenumber"],
["street+place", "housenumber"],
["postcode", "district", "city"]
]
},
@@ -187,6 +210,20 @@
["district"]
]
},
{
"countryCodes": ["ru"],
"format": [
["housenumber", "street+place"],
["city", "postcode"]
]
},
{
"countryCodes": ["cz", "sk"],
"format": [
["street", "housenumber"],
["postcode", "city"]
]
},
{
"countryCodes": ["ph"],
"format": [
+1 -1
View File
@@ -852,7 +852,7 @@ export default {
bubbleIdQuadKey = '0' + bubbleIdQuadKey;
}
const imgUrlPrefix = streetsideImagesApi + 'hs' + bubbleIdQuadKey;
const imgUrlSuffix = '.jpg?g=13305&n=z';
const imgUrlSuffix = '.jpg?g=13515&n=z';
// Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12
const faceKeys = ['01','02','03','10','11','12'];
+146 -78
View File
@@ -36,91 +36,97 @@ export function uiFieldAddress(field, context) {
.catch(function() { /* ignore */ });
function getNearStreets() {
function getNear(isAddressable, type, searchRadius, resultProp) {
var extent = combinedEntityExtent();
var l = extent.center();
var box = geoExtent(l).padByMeters(200);
var box = geoExtent(l).padByMeters(searchRadius);
var streets = context.history().intersects(box)
var features = context.history().intersects(box)
.filter(isAddressable)
.map(function(d) {
var loc = context.projection([
(extent[0][0] + extent[1][0]) / 2,
(extent[0][1] + extent[1][1]) / 2
]);
var choice = geoChooseEdge(context.graph().childNodes(d), loc, context.projection);
.map(d => {
let dist = geoSphericalDistance(d.extent(context.graph()).center(), l);
if (d.type === 'way') {
var loc = context.projection([
(extent[0][0] + extent[1][0]) / 2,
(extent[0][1] + extent[1][1]) / 2
]);
var choice = geoChooseEdge(context.graph().childNodes(d), loc, context.projection);
dist = Math.min(dist, choice.distance);
}
const value = resultProp && d.tags[resultProp] ? d.tags[resultProp] : d.tags.name;
let title = value;
if (type === 'street') {
title = `${addrField.t('placeholders.street')}: ${title}`;
} else if (type === 'place') {
title = `${addrField.t('placeholders.place')}: ${title}`;
}
return {
title: d.tags.name,
value: d.tags.name,
dist: choice.distance
title,
value,
dist,
type,
klass: `address-${type}`
};
})
.sort(function(a, b) {
return a.dist - b.dist;
});
return utilArrayUniqBy(streets, 'value');
return utilArrayUniqBy(features, 'value');
}
function getNearStreets() {
function isAddressable(d) {
return d.tags.highway && d.tags.name && d.type === 'way';
}
return getNear(isAddressable, 'street', 200);
}
function getNearCities() {
var extent = combinedEntityExtent();
var l = extent.center();
var box = geoExtent(l).padByMeters(200);
var cities = context.history().intersects(box)
.filter(isAddressable)
.map(function(d) {
return {
title: d.tags['addr:city'] || d.tags.name,
value: d.tags['addr:city'] || d.tags.name,
dist: geoSphericalDistance(d.extent(context.graph()).center(), l)
};
})
.sort(function(a, b) {
return a.dist - b.dist;
});
return utilArrayUniqBy(cities, 'value');
function getNearPlaces() {
function isAddressable(d) {
if (d.tags.name) {
if (d.tags.admin_level === '8' && d.tags.boundary === 'administrative') return true;
if (d.tags.place) return true;
if (d.tags.boundary === 'administrative' && d.tags.admin_level > 8) return true;
}
return false;
}
return getNear(isAddressable, 'place', 200);
}
function getNearCities() {
function isAddressable(d) {
if (d.tags.name) {
if (d.tags.boundary === 'administrative' && d.tags.admin_level === '8') return true;
if (d.tags.border_type === 'city') return true;
if (d.tags.place === 'city' || d.tags.place === 'town' || d.tags.place === 'village') return true;
}
if (d.tags['addr:city']) return true;
if (d.tags[`${field.key}:city`]) return true;
return false;
}
return getNear(isAddressable, 'city', 200, `${field.key}:city`);
}
function getNearPostcodes() {
return [... new Set([]
.concat(getNearValues('postcode'))
.concat(getNear(d => d.tags.postal_code, 'postcode', 200, 'postal_code')))];
}
function getNearValues(key) {
var extent = combinedEntityExtent();
var l = extent.center();
var box = geoExtent(l).padByMeters(200);
const tagKey = `${field.key}:${key}`;
var results = context.history().intersects(box)
.filter(function hasTag(d) { return _entityIDs.indexOf(d.id) === -1 && d.tags[key]; })
.map(function(d) {
return {
title: d.tags[key],
value: d.tags[key],
dist: geoSphericalDistance(d.extent(context.graph()).center(), l)
};
})
.sort(function(a, b) {
return a.dist - b.dist;
});
function hasTag(d) {
return _entityIDs.indexOf(d.id) === -1 && d.tags[tagKey];
}
return utilArrayUniqBy(results, 'value');
return getNear(hasTag, key, 200, tagKey);
}
@@ -142,11 +148,11 @@ export function uiFieldAddress(field, context) {
var dropdowns = addressFormat.dropdowns || [
'city', 'county', 'country', 'district', 'hamlet',
'neighbourhood', 'place', 'postcode', 'province',
'quarter', 'state', 'street', 'subdistrict', 'suburb'
'quarter', 'state', 'street', 'street+place', 'subdistrict', 'suburb'
];
var widths = addressFormat.widths || {
housenumber: 1/3, street: 2/3,
housenumber: 1/5, unit: 1/5, street: 1/2, place: 1/2,
city: 2/3, state: 1/4, postcode: 1/3
};
@@ -191,16 +197,45 @@ export function uiFieldAddress(field, context) {
function addDropdown(d) {
if (dropdowns.indexOf(d.id) === -1) return; // not a dropdown
var nearValues = (d.id === 'street') ? getNearStreets
: (d.id === 'city') ? getNearCities
: getNearValues;
var nearValues;
switch (d.id) {
case 'street':
nearValues = getNearStreets;
break;
case 'place':
nearValues = getNearPlaces;
break;
case 'street+place':
nearValues = () => []
.concat(getNearStreets())
.concat(getNearPlaces());
d.isAutoStreetPlace = true;
d.id = _tags[`${field.key}:place`] ? 'place' : 'street';
break;
case 'city':
nearValues = getNearCities;
break;
case 'postcode':
nearValues = getNearPostcodes;
break;
default:
nearValues = getNearValues;
}
d3_select(this)
.call(uiCombobox(context, 'address-' + d.id)
.call(uiCombobox(context, `address-${d.isAutoStreetPlace ? 'street-place' : d.id}`)
.minItems(1)
.caseSensitive(true)
.fetcher(function(value, callback) {
callback(nearValues('addr:' + d.id));
.fetcher(function(typedValue, callback) {
typedValue = typedValue.toLowerCase();
callback(nearValues(d.id)
.filter(v => v.value.toLowerCase().indexOf(typedValue) !== -1));
})
.on('accept', function(selected) {
if (d.isAutoStreetPlace) {
// set subtag depending on selected entry
d.id = selected ? selected.type : 'street';
}
})
);
}
@@ -248,42 +283,75 @@ export function uiFieldAddress(field, context) {
function change(onInput) {
return function() {
var tags = {};
setTimeout(() => {
var tags = {};
_wrap.selectAll('input')
.each(function (subfield) {
var key = field.key + ':' + subfield.id;
_wrap.selectAll('input')
.each(function (subfield) {
var key = field.key + ':' + subfield.id;
var value = this.value;
if (!onInput) value = context.cleanTagValue(value);
var value = this.value;
if (!onInput) value = context.cleanTagValue(value);
// don't override multiple values with blank string
if (Array.isArray(_tags[key]) && !value) return;
// don't override multiple values with blank string
if (Array.isArray(_tags[key]) && !value) return;
tags[key] = value || undefined;
});
if (subfield.isAutoStreetPlace) {
if (subfield.id === 'street') {
tags[`${field.key}:place`] = undefined;
} else if (subfield.id === 'place') {
tags[`${field.key}:street`] = undefined;
}
}
dispatch.call('change', this, tags, onInput);
tags[key] = value || undefined;
});
dispatch.call('change', this, tags, onInput);
}, 0);
};
}
function updatePlaceholder(inputSelection) {
return inputSelection.attr('placeholder', function(subfield) {
if (_tags && Array.isArray(_tags[field.key + ':' + subfield.id])) {
return t('inspector.multiple_values');
}
if (_countryCode) {
var localkey = subfield.id + '!' + _countryCode;
var tkey = addrField.hasTextForStringId('placeholders.' + localkey) ? localkey : subfield.id;
return addrField.t('placeholders.' + tkey);
if (subfield.isAutoStreetPlace) {
return `${getLocalPlaceholder('street')} / ${getLocalPlaceholder('place')}`;
}
return getLocalPlaceholder(subfield.id);
});
}
function getLocalPlaceholder(key) {
if (_countryCode) {
var localkey = key + '!' + _countryCode;
var tkey = addrField.hasTextForStringId('placeholders.' + localkey) ? localkey : key;
return addrField.t('placeholders.' + tkey);
}
}
function updateTags(tags) {
utilGetSetValue(_wrap.selectAll('input'), function (subfield) {
var val = tags[field.key + ':' + subfield.id];
utilGetSetValue(_wrap.selectAll('input'), subfield => {
var val;
if (subfield.isAutoStreetPlace) {
const streetKey = `${field.key}:street`;
const placeKey = `${field.key}:place`;
if (tags[streetKey] !== undefined || tags[placeKey] === undefined) {
val = tags[streetKey];
subfield.id = 'street';
} else {
val = tags[placeKey];
subfield.id = 'place';
}
} else {
val = tags[`${field.key}:${subfield.id}`];
}
return typeof val === 'string' ? val : '';
})
.attr('title', function(subfield) {