Simplify feature dictionary handling

Resolves #5219.

Simplify `featureDictionaryFuture` interface in quests
Currently, quests that want to use the feature dictionary, accept a `FutureTask<FeatureDictionary>`, e.g.

https://github.com/streetcomplete/StreetComplete/blob/80781a7f45cddf19223c46ed5ff1a35e3964bb0e/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt#L249

https://github.com/streetcomplete/StreetComplete/blob/80781a7f45cddf19223c46ed5ff1a35e3964bb0e/app/src/main/java/de/westnordost/streetcomplete/quests/amenity_cover/AddAmenityCover.kt#L16-L18

As suggested in https://github.com/streetcomplete/StreetComplete/pull/5212#issuecomment-1693484783 by @westnordost, this interface could be simplified to something like:

```kt
class MyQuest(private val getFeature: (tags: Map<String, String>) -> Feature?) { ... }
```

@qugebert Would you fancy giving it a try?

@FloEdelmann there are lots of small things that could be slightly improved. It doesn't make sense to open issue tickets for all of those. A PR is welcome anyway.
Regarding the feature dictionary, it's worth looking what other uses of the feature dictionary there are and whether it makes sense to simplify these, too.

(Did you pin the iOS ticket #1892 by mistake?)
> It doesn't make sense to open issue tickets for all of those.

Okay, I just thought I'd open an issue so that we don't forget it. Should we maybe open a collection issue with many of those small tasks (maybe also linking to other issues) instead?

> Did you pin the iOS ticket by mistake?

No, I pinned it since it is a highly upvoted issue and contains many actionable tasks, to highlight it for contributors.
> No, I pinned it since it is a highly upvoted issue and contains many actionable tasks, to highlight it for contributors.

Hm well, any task that has some kind of label in this issue tracker should be actionable. So, I'd say as far as you are concerned about that people who want to contribute to the project but don't know what to do, you don't need to worry, as there is enough of it in the issue tracker. That's my view.

Regarding the tasks in the iOS ticket - well I would say, for most tasks I listed there, most of them do not have an additional benefit other than bringing us one step closer to an eventual iOS port. For this reason, I am not sure if it is worth funneling volunteer time into that as long as there is not a big "OK, go!", i.e. someone or an organization leading the charge.
@FloEdelmann
Ok, i can try this but i haven't really understood what i shall do.

Another class that implements all the FeatureDictionary element filtering stuff (Like FeatureYesNoQuest : SimpleYesNoQuest) and AddAmenityCover etc. uses this instead of SimpleYesNoQuest, so we only need to define the quest-specific parameters in AddAmenityCover,AddIsAmentyIndoor,etc. But we still need to pass featureDictionaryFuture in QuestModule?

Or should it work without passing  _featureDictionaryFuture_ as a parameter? But i have no idea yet how to do this, so it would need some time.

> Ok, i can try this but i haven't really understood what i shall do.

Well, don't mind this then.
> > Ok, i can try this but i haven't really understood what i shall do.
>
> Well, don't mind this then.

Ok, I just didn't want to leave it unanswered, since I was addressed directly.

./gradlew :app:testDebugUnitTest --tests "de.westnordost.streetcomplete.quests.shop_type.CheckShopExistenceTest" --tests "de.westnordost.streetcomplete.quests.existence.CheckExistenceTest"

diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt
index 036074509ab..08f4229dab9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt
@@ -1,5 +1,9 @@
 package de.westnordost.streetcomplete.overlays

+import de.westnordost.countryboundaries.CountryBoundaries
+import de.westnordost.osmfeatures.Feature
+import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.streetcomplete.data.meta.CountryInfos
 import de.westnordost.streetcomplete.data.overlays.OverlayRegistry
 import de.westnordost.streetcomplete.overlays.address.AddressOverlay
 import de.westnordost.streetcomplete.overlays.cycleway.CyclewayOverlay
@@ -8,19 +12,37 @@ import de.westnordost.streetcomplete.overlays.sidewalk.SidewalkOverlay
 import de.westnordost.streetcomplete.overlays.street_parking.StreetParkingOverlay
 import de.westnordost.streetcomplete.overlays.surface.SurfaceOverlay
 import de.westnordost.streetcomplete.overlays.way_lit.WayLitOverlay
+import de.westnordost.streetcomplete.util.ktx.getFeature
 import org.koin.core.qualifier.named
 import org.koin.dsl.module
+import java.util.concurrent.FutureTask

 /* Each overlay is assigned an ordinal. This is used for serialization and is thus never changed,
 *  even if the order of overlays is changed.  */
 val overlaysModule = module {
-    single { OverlayRegistry(listOf(
-        0 to WayLitOverlay(),
-        6 to SurfaceOverlay(),
-        1 to SidewalkOverlay(),
-        5 to CyclewayOverlay(get(), get(named("CountryBoundariesFuture"))),
-        2 to StreetParkingOverlay(),
-        3 to AddressOverlay(get(named("CountryBoundariesFuture"))),
-        4 to ShopsOverlay(get(named("FeatureDictionaryFuture"))),
-    )) }
+    single {
+        overlaysRegistry(
+            get(),
+            get(named("CountryBoundariesFuture")),
+            { tags ->
+                get<FutureTask<FeatureDictionary>>(named("FeatureDictionaryFuture"))
+                .get().getFeature(tags)
+            }
+        )
+    }
 }
+
+fun overlaysRegistry(
+    countryInfos: CountryInfos,
+    countryBoundariesFuture: FutureTask<CountryBoundaries>,
+    getFeature: (tags: Map<String, String>) -> Feature?,
+) = OverlayRegistry(listOf(
+
+    0 to WayLitOverlay(),
+    6 to SurfaceOverlay(),
+    1 to SidewalkOverlay(),
+    5 to CyclewayOverlay(countryInfos, countryBoundariesFuture),
+    2 to StreetParkingOverlay(),
+    3 to AddressOverlay(countryBoundariesFuture),
+    4 to ShopsOverlay(getFeature),
+))
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlay.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlay.kt
index eba5fb1e35f..fd7c18a604b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlay.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlay.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.overlays.shops

-import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -16,9 +16,8 @@ import de.westnordost.streetcomplete.quests.place_name.AddPlaceName
 import de.westnordost.streetcomplete.quests.shop_type.CheckShopType
 import de.westnordost.streetcomplete.quests.shop_type.SpecifyShopType
 import de.westnordost.streetcomplete.util.getNameLabel
-import java.util.concurrent.FutureTask

-class ShopsOverlay(private val featureDictionaryFuture: FutureTask<FeatureDictionary>) : Overlay {
+class ShopsOverlay(private val getFeature: (tags: Map<String, String>) -> Feature?) : Overlay {

     override val title = R.string.overlay_shops
     override val icon = R.drawable.ic_quest_shop
@@ -41,10 +40,7 @@ class ShopsOverlay(private val featureDictionaryFuture: FutureTask<FeatureDictio
         mapData
             .filter(IS_SHOP_OR_DISUSED_SHOP_EXPRESSION)
             .map { element ->
-                val feature = featureDictionaryFuture.get()
-                    .byTags(element.tags)
-                    .isSuggestion(false) // no suggestions because we just want the icon
-                    .find().firstOrNull()
+                val feature = getFeature(element.tags)

                 val icon = "ic_preset_" + (feature?.icon ?: "maki-shop" ).replace('-', '_')
                 val label = getNameLabel(element.tags)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt
index 062ed3a592a..277928a29d1 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests

 import de.westnordost.countryboundaries.CountryBoundaries
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.data.meta.CountryInfos
 import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestType
@@ -167,6 +168,7 @@ import de.westnordost.streetcomplete.quests.wheelchair_access.AddWheelchairAcces
 import de.westnordost.streetcomplete.quests.width.AddCyclewayWidth
 import de.westnordost.streetcomplete.quests.width.AddRoadWidth
 import de.westnordost.streetcomplete.screens.measure.ArSupportChecker
+import de.westnordost.streetcomplete.util.ktx.getFeature
 import org.koin.core.qualifier.named
 import org.koin.dsl.module
 import java.util.concurrent.FutureTask
@@ -175,23 +177,28 @@ val questsModule = module {
     factory { RoadNameSuggestionsSource(get()) }
     factory { WayTrafficFlowDao(get()) }

-    single { questTypeRegistry(
-        get(),
-        get(),
-        get(named("FeatureDictionaryFuture")),
-        get(),
-        get(named("CountryBoundariesFuture")),
-        get(),
-    ) }
+    single {
+        questTypeRegistry(
+            get(),
+            get(),
+            get(),
+            get(named("CountryBoundariesFuture")),
+            get(),
+            { tags ->
+                get<FutureTask<FeatureDictionary>>(named("FeatureDictionaryFuture"))
+                    .get().getFeature(tags)
+            }
+        )
+    }
 }

 fun questTypeRegistry(
     trafficFlowSegmentsApi: TrafficFlowSegmentsApi,
     trafficFlowDao: WayTrafficFlowDao,
-    featureDictionaryFuture: FutureTask<FeatureDictionary>,
     countryInfos: CountryInfos,
     countryBoundariesFuture: FutureTask<CountryBoundaries>,
-    arSupportChecker: ArSupportChecker
+    arSupportChecker: ArSupportChecker,
+    getFeature: (tags: Map<String, String>) -> Feature?,
 ) = QuestTypeRegistry(listOf(

     /* The quest types are primarily sorted by how easy they can be solved:
@@ -246,7 +253,7 @@ fun questTypeRegistry(
     9 to AddCarWashType(),

     10 to AddBenchBackrest(),
-    11 to AddAmenityCover(featureDictionaryFuture),
+    11 to AddAmenityCover(getFeature),

     12 to AddBridgeStructure(),

@@ -322,7 +329,7 @@ fun questTypeRegistry(
     /* pulled up in priority to be before CheckExistence because this is basically the check
        whether the postbox is still there in countries in which it is enabled */
     48 to AddPostboxCollectionTimes(),
-    49 to CheckExistence(featureDictionaryFuture),
+    49 to CheckExistence(getFeature),
     155 to AddGritBinSeasonal(),

     50 to AddBoardType(),
@@ -372,9 +379,9 @@ fun questTypeRegistry(
     157 to AddHairdresserCustomers(), // almost always marked on sign outside
     78 to SpecifyShopType(), // above add place name as some brand presets will set the name too
     79 to CheckShopType(),
-    80 to AddPlaceName(featureDictionaryFuture),
-    77 to CheckOpeningHoursSigned(featureDictionaryFuture),
-    81 to AddOpeningHours(featureDictionaryFuture),
+    80 to AddPlaceName(getFeature),
+    77 to CheckOpeningHoursSigned(getFeature),
+    81 to AddOpeningHours(getFeature),
     83 to AddBicyclePump(), // visible from the outside, but only during opening hours

     84 to AddAtmOperator(),
@@ -459,7 +466,7 @@ fun questTypeRegistry(
     132 to AddAcceptsCash(),

     133 to AddFuelSelfService(),
-    156 to CheckShopExistence(featureDictionaryFuture), // after opening hours and similar so they will be preferred if enabled
+    156 to CheckShopExistence(getFeature), // after opening hours and similar so they will be preferred if enabled

     /* ↓ 5.quests that are very numerous ---------------------------------------------------- */

diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/amenity_cover/AddAmenityCover.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/amenity_cover/AddAmenityCover.kt
index f51f03f851b..34a7f1b9105 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/amenity_cover/AddAmenityCover.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/amenity_cover/AddAmenityCover.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.quests.amenity_cover

-import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
@@ -11,10 +11,9 @@ import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.
 import de.westnordost.streetcomplete.osm.Tags
 import de.westnordost.streetcomplete.quests.YesNoQuestForm
 import de.westnordost.streetcomplete.util.ktx.toYesNo
-import java.util.concurrent.FutureTask

 class AddAmenityCover (
-    private val featureDictionaryFuture: FutureTask<FeatureDictionary>
+    private val getFeature: (tags: Map<String, String>) -> Feature?
 ) : OsmElementQuestType<Boolean> {

     private val nodesFilter by lazy { """
@@ -38,17 +37,12 @@ class AddAmenityCover (
     override fun isApplicableTo(element: Element) =
         nodesFilter.matches(element) && hasAnyName(element.tags)

-    private fun hasAnyName(tags: Map<String, String>): Boolean =
-        featureDictionaryFuture.get().byTags(tags).isSuggestion(false).find().isNotEmpty()
+    private fun hasAnyName(tags: Map<String, String>) = getFeature(tags) != null

     override fun getHighlightedElements(element: Element, getMapData: () -> MapDataWithGeometry): Sequence<Element> {
         /* put markers for objects that are exactly the same as for which this quest is asking for
            e.g. it's a ticket validator? -> display other ticket validators. Etc. */
-        val feature = featureDictionaryFuture.get()
-            .byTags(element.tags)
-            .isSuggestion(false) // not brands
-            .find()
-            .firstOrNull() ?: return emptySequence()
+        val feature = getFeature(element.tags) ?: return emptySequence()

         return getMapData().filter { it.tags.containsAll(feature.tags) }.asSequence()
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt
index ab17b3d5018..ac762463ca3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.quests.existence

-import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
@@ -12,10 +12,9 @@ import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.
 import de.westnordost.streetcomplete.osm.LAST_CHECK_DATE_KEYS
 import de.westnordost.streetcomplete.osm.Tags
 import de.westnordost.streetcomplete.osm.updateCheckDate
-import java.util.concurrent.FutureTask

 class CheckExistence(
-    private val featureDictionaryFuture: FutureTask<FeatureDictionary>
+    private val getFeature: (tags: Map<String, String>) -> Feature?
 ) : OsmElementQuestType<Unit> {

     private val nodesFilter by lazy { """
@@ -102,12 +101,7 @@ class CheckExistence(
     override fun getHighlightedElements(element: Element, getMapData: () -> MapDataWithGeometry): Sequence<Element> {
         /* put markers for objects that are exactly the same as for which this quest is asking for
            e.g. it's a ticket validator? -> display other ticket validators. Etc. */
-        val feature = featureDictionaryFuture.get()
-            .byTags(element.tags)
-            .isSuggestion(false) // not brands
-            .find()
-            .firstOrNull() ?: return emptySequence()
-
+        val feature = getFeature(element.tags) ?: return emptySequence()
         return getMapData().filter { it.tags.containsAll(feature.tags) }.asSequence()
     }

@@ -122,8 +116,7 @@ class CheckExistence(
         or ${LAST_CHECK_DATE_KEYS.joinToString(" or ") { "$it < today -$yearsAgo years" }}
     """.trimIndent()

-    private fun hasAnyName(tags: Map<String, String>): Boolean =
-        featureDictionaryFuture.get().byTags(tags).isSuggestion(false).find().isNotEmpty()
+    private fun hasAnyName(tags: Map<String, String>) = getFeature(tags) != null
 }

 private fun <X, Y> Map<X, Y>.containsAll(other: Map<X, Y>) = other.all { this[it.key] == it.value }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt
index 7c076bda7ef..6f891bed91e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.quests.opening_hours

-import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.filters.RelativeDate
 import de.westnordost.streetcomplete.data.elementfilter.filters.TagOlderThan
@@ -17,10 +17,9 @@ import de.westnordost.streetcomplete.osm.opening_hours.parser.isSupportedOpening
 import de.westnordost.streetcomplete.osm.opening_hours.parser.toOpeningHoursRules
 import de.westnordost.streetcomplete.osm.updateCheckDateForKey
 import de.westnordost.streetcomplete.osm.updateWithCheckDate
-import java.util.concurrent.FutureTask

 class AddOpeningHours(
-    private val featureDictionaryFuture: FutureTask<FeatureDictionary>
+    private val getFeature: (tags: Map<String, String>) -> Feature?
 ) : OsmElementQuestType<OpeningHoursAnswer> {

     /* See also AddWheelchairAccessBusiness and AddPlaceName, which has a similar list and is/should
@@ -187,6 +186,5 @@ class AddOpeningHours(
     private fun hasProperName(tags: Map<String, String>): Boolean =
         tags.containsKey("name") || tags.containsKey("brand")

-    private fun hasFeatureName(tags: Map<String, String>): Boolean =
-        featureDictionaryFuture.get().byTags(tags).isSuggestion(false).find().isNotEmpty()
+    private fun hasFeatureName(tags: Map<String, String>) = getFeature(tags) != null
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
index a88b57de1e0..893ce5656dc 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.quests.opening_hours_signed

-import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
@@ -19,10 +19,9 @@ import de.westnordost.streetcomplete.quests.YesNoQuestForm
 import kotlinx.datetime.Instant
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toLocalDateTime
-import java.util.concurrent.FutureTask

 class CheckOpeningHoursSigned(
-    private val featureDictionaryFuture: FutureTask<FeatureDictionary>
+    private val getFeature: (tags: Map<String, String>) -> Feature?
 ) : OsmElementQuestType<Boolean> {

     private val filter by lazy { """
@@ -92,6 +91,5 @@ class CheckOpeningHoursSigned(
     private fun hasProperName(tags: Map<String, String>): Boolean =
         tags.containsKey("name") || tags.containsKey("brand")

-    private fun hasFeatureName(tags: Map<String, String>): Boolean =
-        featureDictionaryFuture.get().byTags(tags).isSuggestion(false).find().isNotEmpty()
+    private fun hasFeatureName(tags: Map<String, String>) = getFeature(tags) != null
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt
index f86cbb2c80b..29454eafcbb 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.quests.place_name

-import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
@@ -12,10 +12,9 @@ import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.
 import de.westnordost.streetcomplete.osm.IS_SHOP_OR_DISUSED_SHOP_EXPRESSION
 import de.westnordost.streetcomplete.osm.Tags
 import de.westnordost.streetcomplete.osm.applyTo
-import java.util.concurrent.FutureTask

 class AddPlaceName(
-    private val featureDictionaryFuture: FutureTask<FeatureDictionary>
+    private val getFeature: (tags: Map<String, String>) -> Feature?
 ) : OsmElementQuestType<PlaceNameAnswer> {

     private val filter by lazy { ("""
@@ -142,6 +141,5 @@ class AddPlaceName(
         }
     }

-    private fun hasFeatureName(tags: Map<String, String>): Boolean =
-        featureDictionaryFuture.get().byTags(tags).isSuggestion(false).find().isNotEmpty()
+    private fun hasFeatureName(tags: Map<String, String>) = getFeature(tags) != null
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistence.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistence.kt
index 0138a5f360f..bf3e395548e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistence.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistence.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.quests.shop_type

-import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.Feature
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
@@ -14,10 +14,9 @@ import de.westnordost.streetcomplete.osm.LAST_CHECK_DATE_KEYS
 import de.westnordost.streetcomplete.osm.Tags
 import de.westnordost.streetcomplete.osm.isShopExpressionFragment
 import de.westnordost.streetcomplete.osm.updateCheckDate
-import java.util.concurrent.FutureTask

 class CheckShopExistence(
-    private val featureDictionaryFuture: FutureTask<FeatureDictionary>
+    private val getFeature: (tags: Map<String, String>) -> Feature?
 ) : OsmElementQuestType<Unit> {
     // opening hours quest acts as a de facto checker of shop existence, but some people disabled it.
     // separate from CheckExistence as very old shop with opening hours should show
@@ -71,6 +70,5 @@ class CheckShopExistence(
     private fun hasProperName(tags: Map<String, String>): Boolean =
         tags.containsKey("name") || tags.containsKey("brand")

-    private fun hasFeatureName(tags: Map<String, String>): Boolean =
-        featureDictionaryFuture.get().byTags(tags).isSuggestion(false).find().isNotEmpty()
+    private fun hasFeatureName(tags: Map<String, String>) = getFeature(tags) != null
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/ktx/FeatureDictionary.kt b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/FeatureDictionary.kt
new file mode 100644
index 00000000000..45bafb2d298
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/FeatureDictionary.kt
@@ -0,0 +1,9 @@
+package de.westnordost.streetcomplete.util.ktx
+
+import de.westnordost.osmfeatures.Feature
+import de.westnordost.osmfeatures.FeatureDictionary
+
+fun FeatureDictionary.getFeature(tags: Map<String, String>): Feature? =
+    this.byTags(tags)
+        .isSuggestion(false) // no brands
+        .find().firstOrNull()

diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceLabelTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/util/NameAndLocationLabelTest.kt
similarity index 83%
rename from app/src/androidTest/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceLabelTest.kt
rename to app/src/androidTest/java/de/westnordost/streetcomplete/util/NameAndLocationLabelTest.kt
index 471e2066655..6a20b45fd34 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceLabelTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/util/NameAndLocationLabelTest.kt
@@ -1,4 +1,4 @@
-package de.westnordost.streetcomplete.quests.existence
+package de.westnordost.streetcomplete.util

 import android.content.res.Configuration
 import android.content.res.Resources
@@ -7,28 +7,22 @@ import de.westnordost.osmfeatures.AndroidFeatureDictionary
 import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
-import de.westnordost.streetcomplete.util.getNameAndLocationLabel
 import java.util.Locale
-import java.util.concurrent.FutureTask
 import kotlin.test.Test
 import kotlin.test.assertEquals

-class CheckExistenceLabelTest {
-    private var featureDictionaryFuture: FutureTask<FeatureDictionary>
+class NameAndLocationLabelTest {
+    private var featureDictionary: FeatureDictionary
     private var englishResources: Resources
-    private var questType: CheckExistence

     init {
         val context = InstrumentationRegistry.getInstrumentation().targetContext
-        featureDictionaryFuture = FutureTask { AndroidFeatureDictionary.create(context.assets, "osmfeatures/default", "osmfeatures/brands") }
-        featureDictionaryFuture.run()
+        featureDictionary = AndroidFeatureDictionary.create(context.assets, "osmfeatures/default", "osmfeatures/brands")

         val conf = Configuration(context.resources.configuration)
         conf.setLocale(Locale.ENGLISH)
         val localizedContext = context.createConfigurationContext(conf)
         englishResources = localizedContext.resources
-
-        questType = CheckExistence(featureDictionaryFuture)
     }

     // https://github.com/streetcomplete/StreetComplete/issues/2512
@@ -90,6 +84,6 @@ class CheckExistenceLabelTest {
         getNameAndLocationLabel(
             Node(0, LatLon(0.0, 0.0), tags),
             englishResources,
-            featureDictionaryFuture.get()
+            featureDictionary
         )?.toString()
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt
index ee1cac3fce6..027d38f2d93 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt
@@ -1,6 +1,5 @@
 package de.westnordost.streetcomplete.quests.existence

-import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
@@ -8,34 +7,14 @@ import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.mock
 import de.westnordost.streetcomplete.testutils.node
-import de.westnordost.streetcomplete.testutils.on
 import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
-import java.util.concurrent.FutureTask
 import kotlin.test.Test
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue

 class CheckExistenceTest {
-    private val questType = CheckExistence(mockOfFeatureDictionary())
-
-    private fun mockOfFeatureDictionary(): FutureTask<FeatureDictionary> {
-        // another option is following CheckExistenceLabelTest
-        // and doing it as an androidTest
-        val matchFound: FeatureDictionary.QueryByTagBuilder = mock()
-        on(matchFound.find()).thenReturn(listOf(mock()))
-        on(matchFound.isSuggestion(false)).thenReturn(matchFound)
-
-        val noMatches: FeatureDictionary.QueryByTagBuilder = mock()
-        on(noMatches.find()).thenReturn(emptyList())
-        on(noMatches.isSuggestion(false)).thenReturn(noMatches)
-
-        val tagFinder: FeatureDictionary = mock()
-        on(tagFinder.byTags(mapOf("amenity" to "telephone"))).thenReturn(matchFound)
-        on(tagFinder.byTags(mapOf("shop" to "weird_value"))).thenReturn(noMatches)
-
-        val tagFinderFutureTask: FutureTask<FeatureDictionary> = mock()
-        on(tagFinderFutureTask.get()).thenReturn(tagFinder)
-        return tagFinderFutureTask
+    private val questType = CheckExistence { tags ->
+        if (tags["amenity"] == "telephone") mock() else null
     }

     @Test fun `apply answer adds check date`() {
@@ -76,13 +55,13 @@ class CheckExistenceTest {
     }

     @Test fun `isApplicableTo returns true for known places with old amenity=telephone`() {
-        val milisecondsFor800Days: Long = 1000L * 60 * 60 * 24 * 800
+        val millisecondsFor800Days: Long = 1000L * 60 * 60 * 24 * 800
         assertTrue(
             questType.isApplicableTo(
                 node(
                     tags = mapOf(
                         "amenity" to "telephone",
-                    ), timestamp = nowAsEpochMilliseconds() - milisecondsFor800Days
+                    ), timestamp = nowAsEpochMilliseconds() - millisecondsFor800Days
                 )
             )
         )
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistenceTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistenceTest.kt
index 16ffa6778ad..5e1583d0c34 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistenceTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopExistenceTest.kt
@@ -1,36 +1,14 @@
 package de.westnordost.streetcomplete.quests.shop_type

-import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.quests.TestMapDataWithGeometry
 import de.westnordost.streetcomplete.testutils.mock
 import de.westnordost.streetcomplete.testutils.node
-import de.westnordost.streetcomplete.testutils.on
-import java.util.concurrent.FutureTask
 import kotlin.test.Test
 import kotlin.test.assertEquals

 class CheckShopExistenceTest {
-    private val questType = CheckShopExistence(mockOfFeatureDictionary())
-
-    private fun mockOfFeatureDictionary(): FutureTask<FeatureDictionary> {
-        // another option is following CheckExistenceLabelTest
-        // and doing it as an androidTest
-        val matchFound: FeatureDictionary.QueryByTagBuilder = mock()
-        on(matchFound.find()).thenReturn(listOf(mock()))
-        on(matchFound.isSuggestion(false)).thenReturn(matchFound)
-
-        val noMatches: FeatureDictionary.QueryByTagBuilder = mock()
-        on(noMatches.find()).thenReturn(emptyList())
-        on(noMatches.isSuggestion(false)).thenReturn(noMatches)
-
-        val tagFinder: FeatureDictionary = mock()
-        on(tagFinder.byTags(mapOf("shop" to "greengrocer", "name" to "Foobar"))).thenReturn(matchFound)
-        on(tagFinder.byTags(mapOf("shop" to "weird_value", "name" to "Foobar"))).thenReturn(noMatches)
-        on(tagFinder.byTags(mapOf("shop" to "weird_value"))).thenReturn(noMatches)
-
-        val tagFinderFutureTask: FutureTask<FeatureDictionary> = mock()
-        on(tagFinderFutureTask.get()).thenReturn(tagFinder)
-        return tagFinderFutureTask
+    private val questType = CheckShopExistence { tags ->
+        if (tags["shop"] == "greengrocer") mock() else null
     }

     @Test


remove geometry filtering from sidewalk and cycleway quest

fixes #5000

Reduce filtering of ways for sidewalk quest
As mentioned in #4915, there are quite a few cases where the sidewalk quest is not asked for ways for which I would expect it to be asked, because of too strict filtering. I understood that there is a filter that ensures that the sidewalk quest is not asked for ways that are believed to have separately mapped sidewalks already by looking for nearby footways that are running in the same direction as the way. Examples mentioned include way https://www.openstreetmap.org/way/862062530 that is not quested because of https://www.openstreetmap.org/way/1056791202 that is of insignificant length, or way https://www.openstreetmap.org/way/23147663 that is not quested because of https://www.openstreetmap.org/way/499159632 that is a path that runs in continuation of it, and https://www.openstreetmap.org/way/673415848 that has a separately mapped sidewalk only on one side (if there is a sidewalk on the other side is unknown).

I think this filter should be reviewed to see:

1. if it's necessary at all. Now that `sidewalk:*=separate` can easily be added by the app, it's not such a waste of time any more. It's actually a good thing to add it, because (as I understood) pedestrian routing apps would like to have info on a separately mapped sidewalk and the road it is related to (I remember reading somewhere the suggestion to add the road name to the sidewalk too, to clarify they are related). https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way states that "the associated street/highway should also be tagged with `sidewalk=separate`" so shouldn't we help doing that with SC?
2. if it can be relaxed or refined, without too much effort, so that fewer roads are excluded from sidewalk questing. One could for instance only take footways tagged with `footway=sidewalk` into account instead of all paths, and demand that separetely mapped sidewalks are present on both sides of the road before excluding it. Determining if a (series of) sidewalk(s) is truly running parallel to a (series of) road(s) for a significant length is probably quite difficult?

> Examples mentioned include way https://www.openstreetmap.org/way/862062530 that is not quested because of https://www.openstreetmap.org/way/1056791202 that is of insignificant length

What if this short way had actually `footway=crossing` tagged? I guess it could be ignored if it had that tagged.

Anyway, since already for some time, IIRC both for the cycleway quest and the sidewalk quests, the answer options "yes, but mapped separately" exist, I wonder if we could remove all these checks and require users to input that it has a separately mapped sidewalk in any case, as you suggest in point 1.

@matkoniecz what do you think? About suggestion 1 and 2?
> Now that sidewalk:*=separate can easily be added by the app, it's not such a waste of time any more. It's actually a good thing to add it, because (as I understood) pedestrian routing apps would like to have info on a separately mapped sidewalk and the road it is related to

That makes sense and would allow to drop some highly complex code resulting sometimes in confusing behaviours.

So I am for it.

> (I remember reading somewhere the suggestion to add the road name to the sidewalk too, to clarify they are related)

that is controversial/region dependent - and valid only if sidewalks have such names ("to clarify they are SOMETHING" is not valid by itself to add `name` tag, name tag is for names only)

> One could for instance only take footways tagged with footway=sidewalk into account instead of all paths,

This does not work well, as often sidewalks are not tagged with this (but if (1) would be accepted then (2) is not needed)

./gradlew :app:testDebugUnitTest --tests "de.westnordost.streetcomplete.quests.cycleway.AddCyclewayTest" --tests "de.westnordost.streetcomplete.quests.sidewalk.AddSidewalkTest" --tests "de.westnordost.streetcomplete.util.math.ElementGeometryMathKtTest"

diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt
index a9bbe1f7173..9e0b4b2b4aa 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt
@@ -9,9 +9,9 @@ import de.westnordost.streetcomplete.data.meta.CountryInfo
 import de.westnordost.streetcomplete.data.meta.CountryInfos
 import de.westnordost.streetcomplete.data.meta.getByLocation
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
-import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
+import de.westnordost.streetcomplete.data.osm.mapdata.filter
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
 import de.westnordost.streetcomplete.data.quest.NoCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
@@ -24,11 +24,7 @@ import de.westnordost.streetcomplete.osm.cycleway.any
 import de.westnordost.streetcomplete.osm.cycleway.applyTo
 import de.westnordost.streetcomplete.osm.cycleway.createCyclewaySides
 import de.westnordost.streetcomplete.osm.cycleway.isAmbiguous
-import de.westnordost.streetcomplete.osm.estimateParkingOffRoadWidth
-import de.westnordost.streetcomplete.osm.estimateRoadwayWidth
-import de.westnordost.streetcomplete.osm.guessRoadwayWidth
 import de.westnordost.streetcomplete.osm.surface.ANYTHING_UNPAVED
-import de.westnordost.streetcomplete.util.math.isNearAndAligned
 import java.util.concurrent.FutureTask

 class AddCycleway(
@@ -42,6 +38,16 @@ class AddCycleway(
     override val achievements = listOf(BICYCLIST)
     override val defaultDisabledMessage = R.string.default_disabled_msg_overlay

+    override fun getHighlightedElements(element: Element, getMapData: () -> MapDataWithGeometry) =
+        getMapData().filter("""
+            ways with (
+                highway ~ cycleway|path
+                or highway ~ footway|bridleway and bicycle ~ yes|designated
+              )
+              and bicycle !~ no|private
+              and access !~ no|private
+        """)
+
     // See overview here: https://ent8r.github.io/blacklistr/?streetcomplete=cycleway/AddCycleway.kt
     // #749. sources:
     // Google Street View (driving around in virtual car)
@@ -77,43 +83,7 @@ class AddCycleway(

     override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable<Element> {
         val eligibleRoads = mapData.ways.filter { roadsFilter.matches(it) }
-
-        /* we want to return two sets of roads: Those that do not have any cycleway tags, and those
-         * that do but have been checked more than 4 years ago. */
-
-        /* For the first, the roadsWithMissingCycleway filter is not enough. In OSM, cycleways may be
-         * mapped as separate ways as well and it is not guaranteed that in this case,
-         * cycleway = separate or something is always tagged on the main road then. So, all roads
-         * should be excluded whose center is within of ~15 meters of a cycleway, to be on the safe
-         * side. */
-
-        val roadsWithMissingCycleway = eligibleRoads.filter { untaggedRoadsFilter.matches(it) }.toMutableList()
-
-        if (roadsWithMissingCycleway.isNotEmpty()) {
-
-            val maybeSeparatelyMappedCyclewayGeometries = mapData.ways
-                .filter { maybeSeparatelyMappedCyclewaysFilter.matches(it) }
-                .mapNotNull { mapData.getWayGeometry(it.id) as? ElementPolylinesGeometry }
-
-            val minAngleToWays = 25.0
-
-            if (maybeSeparatelyMappedCyclewayGeometries.isNotEmpty()) {
-                // filter out roads with missing cycleways that are near footways
-                roadsWithMissingCycleway.removeAll { road ->
-                    val minDistToWays = getMinDistanceToWays(road.tags).toDouble()
-                    val roadGeometry = mapData.getWayGeometry(road.id) as? ElementPolylinesGeometry
-                    roadGeometry?.isNearAndAligned(
-                        minDistToWays,
-                        minAngleToWays,
-                        maybeSeparatelyMappedCyclewayGeometries
-                    ) ?: true
-                }
-            }
-        }
-
-        /* For the second, nothing special. Filter out ways that have been checked less then 4
-        *  years ago or have no known cycleway tags */
-
+        val roadsWithMissingCycleway = eligibleRoads.filter { untaggedRoadsFilter.matches(it) }
         val oldRoadsWithKnownCycleways = eligibleRoads.filter { way ->
             val countryInfo = mapData.getWayGeometry(way.id)?.center?.let { p ->
                 countryInfos.getByLocation(
@@ -128,22 +98,9 @@ class AddCycleway(
         return roadsWithMissingCycleway + oldRoadsWithKnownCycleways
     }

-    private fun getMinDistanceToWays(tags: Map<String, String>): Float =
-        (
-            (estimateRoadwayWidth(tags) ?: guessRoadwayWidth(tags)) +
-            (estimateParkingOffRoadWidth(tags) ?: 0f)
-        ) / 2f +
-        4f // + generous buffer for possible grass verge
-
     override fun isApplicableTo(element: Element): Boolean? {
         if (!roadsFilter.matches(element)) return false
-
-        /* can't determine for yet untagged roads by the tags alone because we need info about
-           surrounding geometry */
-        if (untaggedRoadsFilter.matches(element)) return null
-
-        /* but if already tagged and old, we don't need to look at surrounding geometry to see if
-           it is applicable or not */
+        if (untaggedRoadsFilter.matches(element)) return true
         return element.hasOldInvalidOrAmbiguousCyclewayTags(null)
     }

@@ -180,10 +137,6 @@ private val roadsFilter by lazy { """
       and bicycle != no
       and bicycle != designated
       and access !~ private|no
-      and bicycle != use_sidepath
-      and bicycle:backward != use_sidepath
-      and bicycle:forward != use_sidepath
-      and sidewalk != separate
 """.toElementFilterExpression() }

 // streets that do not have cycleway tagging yet
@@ -206,13 +159,10 @@ private val untaggedRoadsFilter by lazy { """
         or $notInZone30OrLess
       )
       and surface !~ ${ANYTHING_UNPAVED.joinToString("|")}
+      and ~bicycle|bicycle:backward|bicycle:forward !~ use_sidepath
+      and sidewalk != separate
 """.toElementFilterExpression() }

-private val maybeSeparatelyMappedCyclewaysFilter by lazy { """
-    ways with highway ~ path|footway|cycleway|construction
-""".toElementFilterExpression() }
-// highway=construction included, as situation often changes during and after construction
-
 private val notInZone30OrLess = """
    ~"${(MAXSPEED_TYPE_KEYS + "maxspeed").joinToString("|")}" ~ ".*:(urban|rural|trunk|motorway|nsl_single|nsl_dual)"
 """
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt
index 85b2230a09c..ff52cd23e41 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt
@@ -3,103 +3,45 @@ package de.westnordost.streetcomplete.quests.sidewalk
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
-import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
+import de.westnordost.streetcomplete.data.osm.mapdata.filter
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.MAXSPEED_TYPE_KEYS
 import de.westnordost.streetcomplete.osm.Tags
-import de.westnordost.streetcomplete.osm.estimateCycleTrackWidth
-import de.westnordost.streetcomplete.osm.estimateParkingOffRoadWidth
-import de.westnordost.streetcomplete.osm.estimateRoadwayWidth
-import de.westnordost.streetcomplete.osm.guessRoadwayWidth
 import de.westnordost.streetcomplete.osm.sidewalk.LeftAndRightSidewalk
 import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.INVALID
 import de.westnordost.streetcomplete.osm.sidewalk.any
 import de.westnordost.streetcomplete.osm.sidewalk.applyTo
 import de.westnordost.streetcomplete.osm.sidewalk.createSidewalkSides
 import de.westnordost.streetcomplete.osm.surface.ANYTHING_UNPAVED
-import de.westnordost.streetcomplete.util.math.isNearAndAligned

 class AddSidewalk : OsmElementQuestType<LeftAndRightSidewalk> {
-    private val maybeSeparatelyMappedSidewalksFilter by lazy { """
-        ways with
-          highway ~ path|footway|cycleway|construction
-          and foot !~ no|private
-          and access !~ no|private
-    """.toElementFilterExpression() }
-    // highway=construction included, as situation often changes during and after construction
-
     override val changesetComment = "Specify whether roads have sidewalks"
     override val wikiLink = "Key:sidewalk"
     override val icon = R.drawable.ic_quest_sidewalk
     override val achievements = listOf(PEDESTRIAN)
     override val defaultDisabledMessage = R.string.default_disabled_msg_overlay

-    override fun getTitle(tags: Map<String, String>) = R.string.quest_sidewalk_title
-
-    override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable<Element> {
-        val eligibleRoads = mapData.ways.filter { roadsFilter.matches(it) }
-        val roadsWithMissingSidewalks = eligibleRoads.filter { untaggedRoadsFilter.matches(it) }.toMutableList()
-
-        /* Unfortunately, the filter above is not enough. In OSM, sidewalks may be mapped as
-         * separate ways as well and it is not guaranteed that in this case, sidewalk = separate
-         * (or foot = use_sidepath) is always tagged on the main road then. So, all roads should
-         * be excluded whose center is within several meters of a footway, to be on the safe side. */
-
-        if (roadsWithMissingSidewalks.isNotEmpty()) {
-
-            val maybeSeparatelyMappedSidewalkGeometries = mapData.ways
-                .filter { maybeSeparatelyMappedSidewalksFilter.matches(it) }
-                .mapNotNull { mapData.getWayGeometry(it.id) as? ElementPolylinesGeometry }
-            if (maybeSeparatelyMappedSidewalkGeometries.isEmpty()) {
-                return roadsWithMissingSidewalks
-            } else {
-                val minAngleToWays = 25.0
-                // filter out roads with missing sidewalks that are near footways
-                roadsWithMissingSidewalks.removeAll { road ->
-                    val minDistToWays = getMinDistanceToWays(road.tags).toDouble()
-                    val roadGeometry = mapData.getWayGeometry(road.id) as? ElementPolylinesGeometry
-                    roadGeometry?.isNearAndAligned(
-                        minDistToWays,
-                        minAngleToWays,
-                        maybeSeparatelyMappedSidewalkGeometries
-                    ) ?: true
-                }
-            }
-        }
-
-        // Also include any roads with invalid (overloaded) or incomplete sidewalk tagging
-        val roadsWithInvalidSidewalkTags = eligibleRoads.filter { way ->
-            way.hasInvalidOrIncompleteSidewalkTags()
-        }
+    override fun getHighlightedElements(element: Element, getMapData: () -> MapDataWithGeometry) =
+        getMapData().filter("""
+            ways with (
+                highway ~ path|footway|steps
+                or highway ~ cycleway|bridleway and foot ~ yes|designated
+              )
+              and foot !~ no|private
+              and access !~ no|private
+        """)

-        return roadsWithMissingSidewalks + roadsWithInvalidSidewalkTags
-    }
-
-    /* Calculate when footway is too far away from the road to be considered its sidewalk.
-       It is an estimate, and we deliberately err on the side of showing the quest too often. */
-    private fun getMinDistanceToWays(tags: Map<String, String>): Float =
-        (
-            (estimateRoadwayWidth(tags) ?: guessRoadwayWidth(tags)) +
-            (estimateParkingOffRoadWidth(tags) ?: 0f) +
-            (estimateCycleTrackWidth(tags) ?: 0f) +
-            1.5f    // assumed sidewalk width
-        ) / 2f +
-        4f // + generous buffer for possible grass verge
-
-    override fun isApplicableTo(element: Element): Boolean? {
-        if (!roadsFilter.matches(element)) return false
+    override fun getTitle(tags: Map<String, String>) = R.string.quest_sidewalk_title

-        /* can't determine for yet untagged roads by the tags alone because we need info about
-           surrounding geometry */
-        if (untaggedRoadsFilter.matches(element)) return null
+    override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable<Element> =
+        mapData.filter { isApplicableTo(it) }

-        /* but if already tagged and invalid or incomplete, we don't need to look at surrounding
-           geometry to see if it is applicable or not */
-        return element.hasInvalidOrIncompleteSidewalkTags()
-    }
+    override fun isApplicableTo(element: Element): Boolean =
+        roadsFilter.matches(element)
+        && (untaggedRoadsFilter.matches(element) || element.hasInvalidOrIncompleteSidewalkTags())

     override fun createForm() = AddSidewalkForm()

@@ -148,16 +90,11 @@ private val untaggedRoadsFilter by lazy { """
         lit = yes
         or highway = residential
         or ~"${(MAXSPEED_TYPE_KEYS + "maxspeed").joinToString("|")}" ~ ".*:(urban|.*zone.*|nsl_restricted)"
+        or maxspeed <= 60
         or (foot ~ yes|designated and highway ~ motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link)
       )
-      and foot != use_sidepath
-      and bicycle != use_sidepath
-      and bicycle:backward != use_sidepath
-      and bicycle:forward != use_sidepath
-      and cycleway != separate
-      and cycleway:left != separate
-      and cycleway:right != separate
-      and cycleway:both != separate
+      and ~foot|bicycle|bicycle:backward|bicycle:forward !~ use_sidepath
+      and ~cycleway|cycleway:left|cycleway:right|cycleway:both !~ separate
 """.toElementFilterExpression() }

 private fun Element.hasInvalidOrIncompleteSidewalkTags(): Boolean {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt
index 1902ed32c09..58d1011817c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt
@@ -1069,7 +1069,7 @@ class MainFragment :
     }

     private fun showHighlightedElements(quest: OsmQuest, element: Element) {
-        val bbox = quest.geometry.center.enclosingBoundingBox(quest.type.highlightedElementsRadius)
+        val bbox = quest.geometry.getBounds().enlargedBy(quest.type.highlightedElementsRadius)
         var mapData: MapDataWithGeometry? = null

         fun getMapData(): MapDataWithGeometry {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt b/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt
index 0f083907a67..01666b0dc6b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt
@@ -10,44 +10,6 @@ fun ElementPolylinesGeometry.getOrientationAtCenterLineInDegrees(): Float {
     return centerLine.first.initialBearingTo(centerLine.second).toFloat()
 }

-/** Returns whether any individual line segment in this ElementPolylinesGeometry is both within
- *  [maxDistance]m of any line segments of [others] and also
- *  and "aligned", meaning the angle difference is at most the given [maxAngle]
- *
- *  Warning: This is computationally very expensive ( for normal ways, O(n³) ), avoid if possible */
-fun ElementPolylinesGeometry.isNearAndAligned(
-    maxDistance: Double,
-    maxAngle: Double,
-    others: Iterable<ElementPolylinesGeometry>
-): Boolean {
-    val bounds = getBounds().enlargedBy(maxDistance)
-    return others.any { other ->
-        bounds.intersect(other.getBounds())
-        && polylines.any { polyline ->
-            other.polylines.any { otherPolyline ->
-                polyline.isWithinDistanceAndAngleOf(otherPolyline, maxDistance, maxAngle)
-            }
-        }
-    }
-}
-
-private fun List<LatLon>.isWithinDistanceAndAngleOf(other: List<LatLon>, maxDistance: Double, maxAngle: Double): Boolean {
-    asSequenceOfPairs().forEach { (first, second) ->
-        other.asSequenceOfPairs().forEach { (otherFirst, otherSecond) ->
-            val bearing = first.initialBearingTo(second)
-            val otherBearing = otherFirst.initialBearingTo(otherSecond)
-            val bearingDiff = abs(normalizeDegrees(bearing - otherBearing, -180.0))
-            // two ways directly opposite each other should count as aligned
-            val alignmentDiff = if (bearingDiff > 90) 180 - bearingDiff else bearingDiff
-            val distance = first.distanceToArc(otherFirst, otherSecond)
-            if (alignmentDiff <= maxAngle && distance <= maxDistance) {
-                return true
-            }
-        }
-    }
-    return false
-}
-
 fun ElementPolylinesGeometry.intersects(other: ElementPolylinesGeometry): Boolean =
     getBounds().intersect(other.getBounds())
     && polylines.any { polyline ->

diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
index 40b8f7f54f1..23550214349 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
@@ -4,15 +4,12 @@ import de.westnordost.countryboundaries.CountryBoundaries
 import de.westnordost.streetcomplete.data.meta.CountryInfo
 import de.westnordost.streetcomplete.data.meta.CountryInfos
 import de.westnordost.streetcomplete.data.meta.getByLocation
-import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.quests.TestMapDataWithGeometry
 import de.westnordost.streetcomplete.testutils.mock
 import de.westnordost.streetcomplete.testutils.on
-import de.westnordost.streetcomplete.testutils.p
 import de.westnordost.streetcomplete.testutils.pGeom
 import de.westnordost.streetcomplete.testutils.way
 import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
-import de.westnordost.streetcomplete.util.math.translate
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
@@ -47,92 +44,7 @@ class AddCyclewayTest {
         val mapData = TestMapDataWithGeometry(listOf(way))

         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        // because geometry/surroundings are not known
-        assertNull(questType.isApplicableTo(way))
-    }
-
-    @Test fun `not applicable to road with nearby cycleway`() {
-        val mapData = TestMapDataWithGeometry(listOf(
-            way(1L, listOf(1, 2), mapOf(
-                "highway" to "primary",
-                "width" to "18"
-            )),
-            way(2L, listOf(3, 4), mapOf(
-                "highway" to "cycleway"
-            ))
-        ))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(12.999, 135.0)
-        val p4 = p3.translate(50.0, 45.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(0, questType.getApplicableElements(mapData).toList().size)
-    }
-
-    @Test fun `applicable to road with nearby cycleway that is not aligned to the road`() {
-        val mapData = TestMapDataWithGeometry(listOf(
-            way(1L, listOf(1, 2), mapOf(
-                "highway" to "primary",
-                "width" to "18"
-            )),
-            way(2L, listOf(3, 4), mapOf(
-                "highway" to "cycleway"
-            ))
-        ))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(14.0, 135.0)
-        val p4 = p3.translate(50.0, 75.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-    }
-
-    @Test fun `applicable to road with cycleway that is far away enough`() {
-        val mapData = TestMapDataWithGeometry(listOf(
-            way(1L, listOf(1, 2), mapOf(
-                "highway" to "primary",
-                "width" to "18"
-            )),
-            way(2L, listOf(3, 4), mapOf(
-                "highway" to "cycleway"
-            ))
-        ))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(16.0, 135.0)
-        val p4 = p3.translate(50.0, 45.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-    }
-
-    @Test fun `applicable to small road with cycleway that is far away enough`() {
-        val mapData = TestMapDataWithGeometry(listOf(
-            way(1L, listOf(1, 2), mapOf(
-                "highway" to "primary",
-                "lanes" to "2"
-            )),
-            way(2L, listOf(3, 4), mapOf(
-                "highway" to "cycleway"
-            ))
-        ))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(10.0, 135.0)
-        val p4 = p3.translate(50.0, 45.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(1, questType.getApplicableElements(mapData).toList().size)
+        assertTrue(questType.isApplicableTo(way)!!)
     }

     @Test fun `not applicable to road with cycleway=separate`() {
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt
index 8b0bc519295..a3e6dd42d8c 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt
@@ -6,7 +6,6 @@ import de.westnordost.streetcomplete.testutils.p
 import de.westnordost.streetcomplete.testutils.way
 import de.westnordost.streetcomplete.util.math.translate
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Test

@@ -31,7 +30,7 @@ class AddSidewalkTest {
         ))
         val mapData = TestMapDataWithGeometry(listOf(road))
         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertNull(questType.isApplicableTo(road))
+        assertTrue(questType.isApplicableTo(road))
     }

     @Test fun `applicable to road with incomplete sidewalk tagging`() {
@@ -41,7 +40,7 @@ class AddSidewalkTest {
         ))
         val mapData = TestMapDataWithGeometry(listOf(road))
         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertTrue(questType.isApplicableTo(road)!!)
+        assertTrue(questType.isApplicableTo(road))
     }

     @Test fun `applicable to road with invalid sidewalk tagging`() {
@@ -62,7 +61,7 @@ class AddSidewalkTest {
         mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)

         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertTrue(questType.isApplicableTo(road)!!)
+        assertTrue(questType.isApplicableTo(road))
     }

     @Test fun `applicable to road with overloaded sidewalk tagging`() {
@@ -84,97 +83,7 @@ class AddSidewalkTest {
         mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)

         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertTrue(questType.isApplicableTo(road)!!)
-    }
-
-    @Test fun `not applicable to road with nearby footway`() {
-        val road = way(1, listOf(1, 2), mapOf(
-            "highway" to "primary",
-            "lit" to "yes",
-            "width" to "18"
-        ))
-        val footway = way(2, listOf(3, 4), mapOf(
-            "highway" to "footway"
-        ))
-        val mapData = TestMapDataWithGeometry(listOf(road, footway))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(12.999, 135.0)
-        val p4 = p3.translate(50.0, 45.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(0, questType.getApplicableElements(mapData).toList().size)
-        assertNull(questType.isApplicableTo(road))
-    }
-
-    @Test fun `applicable to road with nearby footway that is private`() {
-        val road = way(1, listOf(1, 2), mapOf(
-            "highway" to "primary",
-            "lit" to "yes",
-            "width" to "18"
-        ))
-        val footway = way(2, listOf(3, 4), mapOf(
-            "access" to "private",
-            "highway" to "footway"
-        ))
-        val mapData = TestMapDataWithGeometry(listOf(road, footway))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(12.999, 135.0)
-        val p4 = p3.translate(50.0, 45.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertNull(questType.isApplicableTo(road))
-    }
-
-    @Test fun `applicable to road with nearby footway that is not aligned to the road`() {
-        val road = way(1, listOf(1, 2), mapOf(
-            "highway" to "primary",
-            "lit" to "yes",
-            "width" to "18"
-        ))
-        val footway = way(2, listOf(3, 4), mapOf(
-            "highway" to "footway"
-        ))
-        val mapData = TestMapDataWithGeometry(listOf(road, footway))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(10.0, 135.0)
-        val p4 = p3.translate(50.0, 75.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertNull(questType.isApplicableTo(road))
-    }
-
-    @Test fun `applicable to road with footway that is far away enough`() {
-        val road = way(1L, listOf(1, 2), mapOf(
-            "highway" to "primary",
-            "lit" to "yes",
-            "width" to "18"
-        ))
-        val footway = way(2L, listOf(3, 4), mapOf(
-            "highway" to "footway"
-        ))
-
-        val mapData = TestMapDataWithGeometry(listOf(road, footway))
-        val p1 = p(0.0, 0.0)
-        val p2 = p1.translate(50.0, 45.0)
-        val p3 = p1.translate(16.0, 135.0)
-        val p4 = p3.translate(50.0, 45.0)
-
-        mapData.wayGeometriesById[1L] = ElementPolylinesGeometry(listOf(listOf(p1, p2)), p1)
-        mapData.wayGeometriesById[2L] = ElementPolylinesGeometry(listOf(listOf(p3, p4)), p3)
-
-        assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertNull(questType.isApplicableTo(road))
+        assertTrue(questType.isApplicableTo(road))
     }

     @Test fun `not applicable to motorways`() {
@@ -193,7 +102,7 @@ class AddSidewalkTest {
         ))
         val mapData = TestMapDataWithGeometry(listOf(road))
         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertNull(questType.isApplicableTo(road))
+        assertTrue(questType.isApplicableTo(road))
     }

     @Test fun `applicable to motorways marked as legally accessible to pedestrians and with tagged speed limit`() {
@@ -204,7 +113,7 @@ class AddSidewalkTest {
         ))
         val mapData = TestMapDataWithGeometry(listOf(road))
         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertNull(questType.isApplicableTo(road))
+        assertTrue(questType.isApplicableTo(road))
     }

     @Test fun `not applicable to road with very low speed limit`() {
@@ -219,11 +128,21 @@ class AddSidewalkTest {

     @Test fun `applicable to road with implicit speed limit`() {
         val road = way(tags = mapOf(
-            "highway" to "residential",
+            "highway" to "primary",
             "maxspeed" to "DE:zone30",
         ))
         val mapData = TestMapDataWithGeometry(listOf(road))
         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
-        assertEquals(null, questType.isApplicableTo(road))
+        assertTrue(questType.isApplicableTo(road))
+    }
+
+    @Test fun `applicable to road with urban speed limit`() {
+        val road = way(tags = mapOf(
+            "highway" to "primary",
+            "maxspeed" to "60",
+        ))
+        val mapData = TestMapDataWithGeometry(listOf(road))
+        assertEquals(1, questType.getApplicableElements(mapData).toList().size)
+        assertTrue(questType.isApplicableTo(road))
     }
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/util/math/ElementGeometryMathKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/util/math/ElementGeometryMathKtTest.kt
deleted file mode 100644
index 092be6e89eb..00000000000
--- a/app/src/test/java/de/westnordost/streetcomplete/util/math/ElementGeometryMathKtTest.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package de.westnordost.streetcomplete.util.math
-
-import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
-import de.westnordost.streetcomplete.testutils.p
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class ElementGeometryMathKtTest {
-
-    @Test
-    fun `issue2248 simple`() {
-        // https://github.com/streetcomplete/StreetComplete/issues/2248
-        // this is the geometry of the street and sidewalk boiled down to the necessary elements
-        // to reproduce this bug
-        val street19801348 = ElementPolylinesGeometry(listOf(listOf(
-            p(50.0751820, 19.8861837),
-            p(50.0751033, 19.8865969)
-        )), p(50.0751820, 19.8861837))
-
-        val sidewalk406543797 = ElementPolylinesGeometry(listOf(listOf(
-            p(50.0750588, 19.8866672),
-            p(50.0751290, 19.8862832)
-        )), p(50.0751290, 19.8862832))
-
-        assertTrue(street19801348.isNearAndAligned(10.0, 25.0, listOf(sidewalk406543797)))
-    }
-}