Update city search to return district/locality string suggestions
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s
This commit is contained in:
@@ -128,8 +128,11 @@ AUTH + SYSTEM APIS
|
|||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
|
|
||||||
- Searches city names by prefix from local pincode directory table.
|
- Searches district and locality names by prefix from local pincode directory table.
|
||||||
- Returns unique city + state rows only.
|
- Returns a JSON array of unique strings in this format: "NAME, STATE".
|
||||||
|
- District matches are ranked first.
|
||||||
|
- If district matches exist, locality results from those districts are also included.
|
||||||
|
- Locality suffixes like H.O / S.O / B.O are trimmed in output.
|
||||||
- Uses local DB only (no external API call in this endpoint).
|
- Uses local DB only (no external API call in this endpoint).
|
||||||
|
|
||||||
Request body:
|
Request body:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping
|
|||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class GeoSearch(
|
class GeoSearch(
|
||||||
@@ -20,20 +21,63 @@ class GeoSearch(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
@RequestParam("q") q: String,
|
@RequestParam("q") q: String,
|
||||||
@RequestParam("limit", required = false, defaultValue = "20") limit: Int
|
@RequestParam("limit", required = false, defaultValue = "20") limit: Int
|
||||||
): List<CityStateResponse> {
|
): List<String> {
|
||||||
requirePrincipal(principal)
|
requirePrincipal(principal)
|
||||||
val query = q.trim()
|
val query = q.trim()
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "q must be at least 2 characters")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "q must be at least 2 characters")
|
||||||
}
|
}
|
||||||
val boundedLimit = limit.coerceIn(1, 100)
|
val boundedLimit = limit.coerceIn(1, 100)
|
||||||
return indiaPincodeCityStateRepo
|
val districtRows = indiaPincodeCityStateRepo
|
||||||
.searchCityStateByPrefix(query, PageRequest.of(0, boundedLimit))
|
.searchDistrictStateByPrefix(query, PageRequest.of(0, boundedLimit))
|
||||||
.map { CityStateResponse(city = it.city, state = it.state) }
|
val localityPrefixRows = indiaPincodeCityStateRepo
|
||||||
|
.searchLocalityStateByPrefix(query, PageRequest.of(0, boundedLimit))
|
||||||
|
|
||||||
|
val results = linkedSetOf<String>()
|
||||||
|
districtRows.forEach { row ->
|
||||||
|
val district = row.district.trim().ifBlank { return@forEach }
|
||||||
|
val state = row.state.trim().ifBlank { return@forEach }
|
||||||
|
results.add("$district, $state")
|
||||||
|
}
|
||||||
|
localityPrefixRows.forEach { row ->
|
||||||
|
val formatted = formatLocalityWithState(row.locality, row.state) ?: return@forEach
|
||||||
|
results.add(formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (districtRows.isNotEmpty() && results.size < boundedLimit) {
|
||||||
|
val districtKeys = districtRows
|
||||||
|
.map { it.district.trim().uppercase(Locale.ROOT) }
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.toSet()
|
||||||
|
if (districtKeys.isNotEmpty()) {
|
||||||
|
val extraRows = indiaPincodeCityStateRepo.findLocalityStateByDistricts(
|
||||||
|
districtKeys,
|
||||||
|
PageRequest.of(0, boundedLimit * 5)
|
||||||
|
)
|
||||||
|
extraRows.forEach { row ->
|
||||||
|
if (results.size >= boundedLimit) return@forEach
|
||||||
|
val formatted = formatLocalityWithState(row.locality, row.state) ?: return@forEach
|
||||||
|
results.add(formatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.take(boundedLimit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CityStateResponse(
|
private fun formatLocalityWithState(localityRaw: String, stateRaw: String): String? {
|
||||||
val city: String,
|
val state = stateRaw.trim()
|
||||||
val state: String
|
if (state.isBlank()) return null
|
||||||
)
|
val locality = localityRaw
|
||||||
|
.trim()
|
||||||
|
.replace(LOCALITY_SUFFIX_REGEX, "")
|
||||||
|
.replace(WHITESPACE_REGEX, " ")
|
||||||
|
.trim()
|
||||||
|
if (locality.isBlank()) return null
|
||||||
|
return "$locality, $state"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val LOCALITY_SUFFIX_REGEX =
|
||||||
|
Regex("""\s+(H\.O|S\.O|B\.O|G\.P\.O|PO|HO|SO|BO|GPO)(\s*\(.*\))?$""", RegexOption.IGNORE_CASE)
|
||||||
|
private val WHITESPACE_REGEX = Regex("\\s+")
|
||||||
|
|||||||
@@ -42,6 +42,57 @@ interface IndiaPincodeCityStateRepo : JpaRepository<IndiaPincodeCityState, UUID>
|
|||||||
@Param("prefix") prefix: String,
|
@Param("prefix") prefix: String,
|
||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): List<CityStateRow>
|
): List<CityStateRow>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
select p.city as district,
|
||||||
|
p.state as state
|
||||||
|
from IndiaPincodeCityState p
|
||||||
|
where trim(p.city) <> ''
|
||||||
|
and trim(p.state) <> ''
|
||||||
|
and lower(p.city) like lower(concat(:prefix, '%'))
|
||||||
|
group by p.city, p.state
|
||||||
|
order by p.city asc, p.state asc
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun searchDistrictStateByPrefix(
|
||||||
|
@Param("prefix") prefix: String,
|
||||||
|
pageable: Pageable
|
||||||
|
): List<DistrictStateRow>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
select p.locality as locality,
|
||||||
|
p.state as state
|
||||||
|
from IndiaPincodeCityState p
|
||||||
|
where trim(p.locality) <> ''
|
||||||
|
and trim(p.state) <> ''
|
||||||
|
and lower(p.locality) like lower(concat(:prefix, '%'))
|
||||||
|
group by p.locality, p.state
|
||||||
|
order by p.locality asc, p.state asc
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun searchLocalityStateByPrefix(
|
||||||
|
@Param("prefix") prefix: String,
|
||||||
|
pageable: Pageable
|
||||||
|
): List<LocalityStateRow>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
select p.locality as locality,
|
||||||
|
p.state as state
|
||||||
|
from IndiaPincodeCityState p
|
||||||
|
where trim(p.locality) <> ''
|
||||||
|
and trim(p.state) <> ''
|
||||||
|
and upper(trim(p.city)) in :districts
|
||||||
|
group by p.locality, p.state
|
||||||
|
order by p.locality asc, p.state asc
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun findLocalityStateByDistricts(
|
||||||
|
@Param("districts") districts: Set<String>,
|
||||||
|
pageable: Pageable
|
||||||
|
): List<LocalityStateRow>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PincodeCityStateCandidate {
|
interface PincodeCityStateCandidate {
|
||||||
@@ -54,3 +105,13 @@ interface CityStateRow {
|
|||||||
val city: String
|
val city: String
|
||||||
val state: String
|
val state: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DistrictStateRow {
|
||||||
|
val district: String
|
||||||
|
val state: String
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalityStateRow {
|
||||||
|
val locality: String
|
||||||
|
val state: String
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user