From 70cf8bf4268df1e85ca619c2717c932b47a46d21 Mon Sep 17 00:00:00 2001 From: Ellen Poe Date: Tue, 25 Nov 2025 17:26:41 -0800 Subject: [PATCH] feat: allow users to re-run search if there are no results --- .../cardinal/geocoding/GeocodingService.kt | 14 ++++-- .../geocoding/MultiplexedGeocodingService.kt | 14 ++++-- .../geocoding/OfflineGeocodingService.kt | 6 ++- .../geocoding/PeliasGeocodingService.kt | 13 ++++- .../cardinal/ui/core/BaseSearchViewModel.kt | 10 +++- .../ui/directions/DirectionsScreen.kt | 10 +++- .../earth/maps/cardinal/ui/home/HomeScreen.kt | 10 ++++ .../maps/cardinal/ui/place/SearchResults.kt | 47 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 4 ++ 9 files changed, 115 insertions(+), 13 deletions(-) diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/GeocodingService.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/GeocodingService.kt index 0f9d869..185a802 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/GeocodingService.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/GeocodingService.kt @@ -32,8 +32,8 @@ abstract class GeocodingService(private val locationRepository: LocationReposito * @param focusPoint Optional focus point for viewport biasing * @return Place objects */ - suspend fun geocode(query: String, focusPoint: LatLng? = null): List { - return convertResultsToPlaces(geocodeRaw(query, focusPoint)) + suspend fun geocode(query: String, focusPoint: LatLng? = null, autocomplete: Boolean = true): List { + return convertResultsToPlaces(geocodeRaw(query, focusPoint, autocomplete)) } /** @@ -46,6 +46,13 @@ abstract class GeocodingService(private val locationRepository: LocationReposito return convertResultsToPlaces(reverseGeocodeRaw(latitude, longitude)) } + /** + * Query whether the is service has separate autocomplete behavior. + * @return true if this geocoding service has separate behavior for autocomplete and "normal" + * full-text search. false otherwise. + */ + abstract fun hasSeparateAutocomplete(): Boolean + /** * Find nearby places around a given point, returning Place objects. * @param latitude The latitude coordinate @@ -76,7 +83,8 @@ abstract class GeocodingService(private val locationRepository: LocationReposito */ abstract suspend fun geocodeRaw( query: String, - focusPoint: LatLng? = null + focusPoint: LatLng? = null, + autocomplete: Boolean ): List /** diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/MultiplexedGeocodingService.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/MultiplexedGeocodingService.kt index 84435eb..f360ea3 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/MultiplexedGeocodingService.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/MultiplexedGeocodingService.kt @@ -30,11 +30,11 @@ class MultiplexedGeocodingService( locationRepository: LocationRepository, ) : GeocodingService(locationRepository) { - override suspend fun geocodeRaw(query: String, focusPoint: LatLng?): List { + override suspend fun geocodeRaw(query: String, focusPoint: LatLng?, autocomplete: Boolean): List { return if (appPreferenceRepository.offlineMode.value) { - offlineGeocodingService.geocodeRaw(query, focusPoint) + offlineGeocodingService.geocodeRaw(query, focusPoint, autocomplete) } else { - onlineGeocodingService.geocodeRaw(query, focusPoint) + onlineGeocodingService.geocodeRaw(query, focusPoint, autocomplete) } } @@ -60,4 +60,12 @@ class MultiplexedGeocodingService( onlineGeocodingService.nearbyRaw(latitude, longitude, selectedCategories) } } + + override fun hasSeparateAutocomplete(): Boolean { + return if (appPreferenceRepository.offlineMode.value) { + offlineGeocodingService.hasSeparateAutocomplete() + } else { + onlineGeocodingService.hasSeparateAutocomplete() + } + } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/OfflineGeocodingService.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/OfflineGeocodingService.kt index 4aa84a1..b72b88b 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/OfflineGeocodingService.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/OfflineGeocodingService.kt @@ -35,7 +35,7 @@ class OfflineGeocodingService( private val geocoderDir = File(context.filesDir, "geocoder").apply { mkdirs() } private val airmailIndex = newAirmailIndex("en", geocoderDir.absolutePath) - override suspend fun geocodeRaw(query: String, focusPoint: LatLng?): List { + override suspend fun geocodeRaw(query: String, focusPoint: LatLng?, autocomplete: Boolean): List { try { val results = airmailIndex.searchPhrase(query) val geocodeResults = results.map { poi -> @@ -132,6 +132,10 @@ class OfflineGeocodingService( ) } + override fun hasSeparateAutocomplete(): Boolean { + return false + } + companion object { const val TAG = "OfflineGeocodingService" } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/PeliasGeocodingService.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/PeliasGeocodingService.kt index a90c634..aa56172 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/PeliasGeocodingService.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/geocoding/PeliasGeocodingService.kt @@ -58,11 +58,16 @@ class PeliasGeocodingService( install(Logging) } - override suspend fun geocodeRaw(query: String, focusPoint: LatLng?): List { + override suspend fun geocodeRaw(query: String, focusPoint: LatLng?, autocomplete: Boolean): List { try { Log.d(TAG, "Geocoding query: $query, focusPoint: $focusPoint") val config = appPreferenceRepository.peliasApiConfig.value - val response = client.get("${config.baseUrl}/autocomplete") { + val endpoint = if (autocomplete) { + "${config.baseUrl}/autocomplete" + } else { + "${config.baseUrl}/search" + } + val response = client.get(endpoint) { parameter("text", query) parameter("size", "10") config.apiKey?.let { parameter("api_key", it) } @@ -200,4 +205,8 @@ class PeliasGeocodingService( null } } + + override fun hasSeparateAutocomplete(): Boolean { + return true + } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/BaseSearchViewModel.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/BaseSearchViewModel.kt index 9ee1762..12fdcc7 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/BaseSearchViewModel.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/BaseSearchViewModel.kt @@ -64,6 +64,8 @@ abstract class BaseSearchViewModel( var searchError by mutableStateOf(null) protected set + val expandedResultsAvailable: Boolean get() = geocodingService.hasSeparateAutocomplete() + init { // Set up debounced search searchQueryFlow @@ -93,14 +95,14 @@ abstract class BaseSearchViewModel( * Performs the actual search operation * Can be overridden by subclasses to provide custom focus point logic */ - protected open fun performSearch(query: String) { + protected open fun performSearch(query: String, autoComplete: Boolean = true) { viewModelScope.launch { isSearching = true searchError = null try { // Get focus point for viewport biasing - subclasses can override this val focusPoint = getSearchFocusPoint() - geocodeResults.value = geocodingService.geocode(query, focusPoint) + geocodeResults.value = geocodingService.geocode(query, focusPoint, autoComplete) isSearching = false } catch (e: Exception) { // Handle error @@ -135,4 +137,8 @@ abstract class BaseSearchViewModel( open fun onPlaceSelected(place: Place) { addRecentSearch(place) } + + fun rerunWithoutAutocomplete() { + performSearch(searchQuery, autoComplete = false) + } } \ No newline at end of file diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt index 18e1070..74d0805 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt @@ -636,10 +636,16 @@ private fun SearchResultsContent( fieldFocusState: FieldFocusState ) { SearchResults( - places = viewModel.geocodeResults.value, onPlaceSelected = { place -> + places = viewModel.geocodeResults.value, + onPlaceSelected = { place -> updatePlaceForField(viewModel, fieldFocusState, place) onFieldFocusStateChange(FieldFocusState.NONE) - }, modifier = Modifier.fillMaxWidth() + }, + expandedResultsAvailable = viewModel.expandedResultsAvailable, + onShowExpandedResults = { + viewModel.rerunWithoutAutocomplete() + }, + modifier = Modifier.fillMaxWidth() ) } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt index 9b15490..5eb4feb 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt @@ -73,6 +73,7 @@ import earth.maps.cardinal.data.GeocodeResult import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.room.RecentSearch import earth.maps.cardinal.ui.core.TOOLBAR_HEIGHT_DP +import earth.maps.cardinal.ui.place.ExpandSearchResultsCard import earth.maps.cardinal.ui.place.SearchResultItem import kotlinx.coroutines.launch import kotlin.math.abs @@ -299,6 +300,7 @@ private fun ContentBelow( ) { val recentSearches = remember { mutableStateOf>(emptyList()) } val coroutineScope = rememberCoroutineScope() + val expandedResultsAvailable = viewModel.expandedResultsAvailable LaunchedEffect(Unit) { coroutineScope.launch { @@ -317,6 +319,14 @@ private fun ContentBelow( onPlaceSelected ) } + if (geocodePlaces.isEmpty() && expandedResultsAvailable) { + item { + ExpandSearchResultsCard { + viewModel.rerunWithoutAutocomplete() + } + } + } + } Spacer(modifier = Modifier.fillMaxSize()) } else { diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt index 157bdb7..d205d3c 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -53,6 +54,8 @@ import earth.maps.cardinal.data.format fun SearchResults( places: List, onPlaceSelected: (Place) -> Unit, + expandedResultsAvailable: Boolean, + onShowExpandedResults: () -> Unit, modifier: Modifier = Modifier ) { val addressFormatter = remember { AddressFormatter() } @@ -64,9 +67,53 @@ fun SearchResults( onPlaceSelected = onPlaceSelected ) } + if (places.isEmpty() && expandedResultsAvailable) { + item { + ExpandSearchResultsCard(onShowExpandedResults) + } + } } } +@Composable +fun ExpandSearchResultsCard( + onShowExpandedResults: () -> Unit, +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = dimensionResource(dimen.padding)) + .clickable { + onShowExpandedResults() + }, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(dimensionResource(dimen.padding)), + verticalAlignment = Alignment.CenterVertically + ) { + // Re-run search. + Column( + modifier = Modifier + .weight(1f) + .padding(start = dimensionResource(dimen.padding)) + ) { + Text( + text = stringResource(R.string.search_again), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(R.string.search_again_details), + ) + } + } + } + +} + @Composable fun SearchResultItem( addressFormatter: AddressFormatter, diff --git a/cardinal-android/app/src/main/res/values/strings.xml b/cardinal-android/app/src/main/res/values/strings.xml index 983f34f..1bb3228 100644 --- a/cardinal-android/app/src/main/res/values/strings.xml +++ b/cardinal-android/app/src/main/res/values/strings.xml @@ -265,6 +265,7 @@ Entertainment Nightlife Cannot paste a list into itself or one of its sublists + Open. Closed. Closes %1$s. @@ -286,4 +287,7 @@ Today Hours Day + + Show additional results + Include results that don\'t exactly match -- GitLab