diff --git a/docs/API_REFERENCE.txt b/docs/API_REFERENCE.txt index 3d90232..dcb22d1 100644 --- a/docs/API_REFERENCE.txt +++ b/docs/API_REFERENCE.txt @@ -128,8 +128,11 @@ AUTH + SYSTEM APIS What it does: - - Searches city names by prefix from local pincode directory table. - - Returns unique city + state rows only. + - Searches district and locality names by prefix from local pincode directory table. + - 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). Request body: diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/system/GeoSearch.kt b/src/main/kotlin/com/android/trisolarisserver/controller/system/GeoSearch.kt index 27aa333..40f3e29 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/system/GeoSearch.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/system/GeoSearch.kt @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException +import java.util.Locale @RestController class GeoSearch( @@ -20,20 +21,63 @@ class GeoSearch( @AuthenticationPrincipal principal: MyPrincipal?, @RequestParam("q") q: String, @RequestParam("limit", required = false, defaultValue = "20") limit: Int - ): List { + ): List { requirePrincipal(principal) val query = q.trim() if (query.length < 2) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "q must be at least 2 characters") } val boundedLimit = limit.coerceIn(1, 100) - return indiaPincodeCityStateRepo - .searchCityStateByPrefix(query, PageRequest.of(0, boundedLimit)) - .map { CityStateResponse(city = it.city, state = it.state) } + val districtRows = indiaPincodeCityStateRepo + .searchDistrictStateByPrefix(query, PageRequest.of(0, boundedLimit)) + val localityPrefixRows = indiaPincodeCityStateRepo + .searchLocalityStateByPrefix(query, PageRequest.of(0, boundedLimit)) + + val results = linkedSetOf() + 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( - val city: String, - val state: String -) +private fun formatLocalityWithState(localityRaw: String, stateRaw: String): String? { + val state = stateRaw.trim() + 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+") diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/property/IndiaPincodeCityStateRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/property/IndiaPincodeCityStateRepo.kt index 81d09e5..15ec445 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/property/IndiaPincodeCityStateRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/property/IndiaPincodeCityStateRepo.kt @@ -42,6 +42,57 @@ interface IndiaPincodeCityStateRepo : JpaRepository @Param("prefix") prefix: String, pageable: Pageable ): List + + @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 + + @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 + + @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, + pageable: Pageable + ): List } interface PincodeCityStateCandidate { @@ -54,3 +105,13 @@ interface CityStateRow { val city: String val state: String } + +interface DistrictStateRow { + val district: String + val state: String +} + +interface LocalityStateRow { + val locality: String + val state: String +}