Repository: streetcomplete/StreetComplete (language: kotlin)

Issue:
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?


Discussion hints:
> 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)

Pull request: remove geometry filtering from sidewalk and cycleway quest
fixes #5000

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

Fix patch:
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 ->


Test patch:
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)))
-    }
-}