# Add city autocomplete to a form

A free text field for the city gives you `NYC`, `New York`, and `new york city` for one city, and typos for many others. A city field with autocomplete gives you one ID for each city.

## Create a public key

1. On the dashboard, create a public key.
2. Add the origin of your site, such as `https://www.example.com`.
3. Add the origin of your local development server, such as `http://localhost:4321`.

[Keys and origins](/docs/keys) gives the rules for origins.

## Request cities while the person types

Send the text of the field to [Autocomplete](/docs/api#autocompleteCities). Autocomplete returns cities with a name that starts with the text. If no city matches a text of 4 or more characters, autocomplete also accepts a name with one typo.

Each successful response costs one unit, so do not send a request for each key press:

- Do not send a text of fewer than 2 characters. The API returns no cities for this text, and the response costs one unit.
- Wait for a short pause in the typing before you send the request.
- Cancel the previous request when the text changes, so that an old response does not replace a new one.

```js
const AUTOCOMPLETE_URL = 'https://api.onde.earth/v1/cities/autocomplete'
const PUBLIC_KEY = 'onde_pk_...'
const MINIMUM_LENGTH = 2
const INPUT_DELAY_MS = 150

const input = form.elements.city
const cityId = form.elements.cityId
let timer
let controller

async function suggestCities(text, signal) {
  const url = new URL(AUTOCOMPLETE_URL)
  url.searchParams.set('q', text)
  url.searchParams.set('key', PUBLIC_KEY)
  const response = await fetch(url, { signal })
  const body = await response.json()
  if (!response.ok) {
    throw new Error(body.detail)
  }
  return body.data
}

input.addEventListener('input', () => {
  cityId.value = ''
  clearTimeout(timer)
  controller?.abort()
  const text = input.value.trim()
  if (text.length < MINIMUM_LENGTH) {
    showCities([])
    return
  }
  timer = setTimeout(async () => {
    controller = new AbortController()
    try {
      showCities(await suggestCities(text, controller.signal))
    } catch (error) {
      if (error.name !== 'AbortError') {
        showCities([])
      }
    }
  }, INPUT_DELAY_MS)
})
```

`form` is your form, with a text field `city` and a hidden field `cityId`. `showCities` is a function of your interface that shows the list. If a request fails, the list stays empty, and the person can type the city as text.

Each change of the text clears `cityId`. Without this step, a person who selects Paris and then types Berlin sends the ID of Paris.

## Show each city

Many cities have the same name. Show the region and the country with each name, so that the person can find the correct city.

```js
function cityLabel(city) {
  return [city.name, city.region?.name, city.country.name]
    .filter(Boolean)
    .join(', ')
}
```

`region` is `null` for a city with no region. A result can also be a district, such as Manhattan. A district has `type` `district`, and `parentCityId` is the ID of its city.

Show the list with the [combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) of the WAI-ARIA Authoring Practices, so that a person can select a city with the keyboard. A combobox component of your framework can show the list.

## Store the selected city

When the person selects a city, put its ID in the hidden field.

```js
function selectCity(city) {
  input.value = cityLabel(city)
  cityId.value = city.id
  showCities([])
}
```

```html
<input type="hidden" name="cityId">
```

On the server, send the ID to [Get a city](/docs/api#getCity) with a secret key. If the status is not 200, the ID is not valid, so ask the person to select the city again. The response also gives all fields of the city. [Store cities in your database](/docs/stored-cities) tells which fields to keep.

## Accept text with no selection

A person can submit the form with no selection, for example when the API refused a request. Then `cityId` is empty, and `city` holds the text.

On the server, send the text to [Search](/docs/api#searchCities). Accept the first result only if its `match` is `exact` or `alias`, and no other result has `exact` or `alias`. Otherwise, store the text and ask the person to select the city later. [Match city names from a file](/docs/search#match-city-names-from-a-file) gives the same rule.

## Set the language and the countries

These parameters of autocomplete change the results:

- `language` sets the language of the names. Use the language of the page.
- `country` limits the results to the countries in the list, such as `country=PT,ES`. Use it if your service is only in some countries.
- `worldview` sets the country of a city in a disputed area. [Worldviews and disputed areas](/docs/worldviews) tells how to select a worldview.
