The SearchAddress component provides a flexible and interactive search interface for addresses, utilizing the powerful Nominatim service from OpenStreetMap. This component is built with React and integrates UI components from shadcn-ui and icons from lucide-react.
- Autocomplete Search: Offers suggestions as you type, reducing the need for full address inputs and improving user experience.
- Grouped Results: Displays search results grouped by address type, making it easier to find the exact location.
- Interactive UI: Uses a popover to display search results that can be selected via click or touch interaction.
- Debounced Input: Improves performance by debouncing the search input.
- Multiple Providers Support: Uses
leaflet-geosearchto support multiple geocoding providers.
First, ensure that shadcn-ui components are added to your project. If not already installed, you can add them using the following command:
npx shadcn-ui@latest add button command popoverAdditionally, install lucide-react for using icons:
npm install lucide-reactOr using yarn:
yarn add lucide-reactNext, install leaflet-geosearch for geocoding support:
npm install leaflet-geosearchOr using yarn:
yarn add leaflet-geosearchCopy the component file into your project:
- Download or copy the file
search-address.tsxfrom the repository. - Place the file in your project directory, typically under
components/ui/.
Copy the hook files into your project:
- Download or copy the files
use-debounce.tsanduse-search-address.tsfrom the repository. - Place the files in your project directory, typically under
hooks/.
-
Import the Component:
Include the
SearchAddresscomponent in your React application by importing it:import dynamic from "next/dynamic"; const SearchAddress = dynamic(() => import("@/components/ui/search-address"), { ssr: false, });
Using dynamic imports with SSR disabled helps avoid the
window is not definederror during server-side rendering. -
Utilize the Component:
You can use the
SearchAddresscomponent anywhere within your React application:function App() { return ( <div className="App"> <SearchAddress onSelectLocation={(location) => console.log(location)} /> </div> ); }
This hook debounces a value, improving performance by limiting the number of times a function is called.
import * as React from "react";
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}This hook encapsulates the search logic and state management for the SearchAddress component.
import { useState, useCallback, useEffect } from "react";
import { OpenStreetMapProvider } from "leaflet-geosearch";
import { RawResult } from "leaflet-geosearch/dist/providers/bingProvider.js";
import { SearchResult } from "leaflet-geosearch/dist/providers/provider.js";
import { useDebounce } from "./use-debounce";
interface UseSearchAddressResult {
query: string;
results: Record<string, SearchResult<RawResult>[]>;
loading: boolean;
handleSearch: (value: string) => void;
selectedItem: SearchResult<RawResult> | null;
setSelectedItem: (item: SearchResult<RawResult> | null) => void;
}
export const useSearchAddress = (): UseSearchAddressResult => {
const [query, setQuery] = useState("");
const [results, setResults] = useState<
Record<string, SearchResult<RawResult>[]>
>({});
const [loading, setLoading] = useState(false);
const [selectedItem, setSelectedItem] =
useState<SearchResult<RawResult> | null>(null);
const debouncedQuery = useDebounce(query, 500);
const groupByType = useCallback(
(data: SearchResult<RawResult>[]): Record<string, SearchResult<RawResult>[]> => {
return data.reduce(
(acc, item) => {
const { raw } = item;
const rawData = raw as unknown as any;
const type = rawData.class;
if (!acc[type]) {
acc[type] = [];
}
acc[type]?.push(item);
return acc;
},
{} as Record<string, SearchResult<RawResult>[]>
);
},
[]
);
const handleSearch = (value: string) => {
setQuery(value);
};
useEffect(() => {
const fetchResults = async () => {
if (debouncedQuery.length > 2) {
setLoading(true);
const provider = new OpenStreetMapProvider();
const results = await provider.search({ query: debouncedQuery });
setResults(groupByType(results as unknown as SearchResult<RawResult>[]));
setLoading(false);
} else {
setResults({});
}
};
fetchResults();
}, [debouncedQuery, groupByType]);
return {
query,
results,
loading,
handleSearch,
selectedItem,
setSelectedItem,
};
};This component relies on the following external libraries and styles:
shadcn-uifor UI components such as Button, Command, and Popover.lucide-reactfor icons likeCheckandChevronsUpDown.leaflet-geosearchfor geocoding support with multiple providers.
The component makes requests to the https://nominatim.openstreetmap.org/search endpoint. Ensure that any API usage complies with the Nominatim usage policy, particularly regarding the custom HTTP header requirements for identification.
For configuration options and details on supported providers, refer to the leaflet-geosearch documentation.
Contributions to enhance or fix issues in the SearchAddress component are welcome. Please follow the standard pull request process for this repository.
This component is available under the MIT License. See the LICENSE file in the repository for full license text.

