@geosdi/ngx-geocoding 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,59 +1,253 @@
1
- # NgxGeocoding
1
+ # @geosdi/ngx-geocoding
2
2
 
3
- This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.0.0.
3
+ Address search for Angular, from one or more sources tried **in order**.
4
4
 
5
- # General information
5
+ Out of the box it queries **Nominatim** (OpenStreetMap), which needs no API key
6
+ and is what this library has always done. **Google** can be added on top: the
7
+ library never sees your key, it uses the **Maps JS SDK** the host application
8
+ already loads in the page.
6
9
 
7
- Component to get a list of place with a geocoding service using nominatim of openstreet map
10
+ A source that is not usable is **skipped, and that is not an error**: the same
11
+ configuration therefore works both on a deploy that has a Google key and on one
12
+ that does not.
8
13
 
9
14
  ## Installation
10
15
 
11
16
  `npm i @geosdi/ngx-geocoding --save`
12
17
 
13
- It depends on :
14
-
15
- "@angular/common": ">=13.0.0",
16
- "@angular/core": ">=13.0.0",
17
- "typescript": ">=4.4.4",
18
- "rxjs": ">=7.0.0"
18
+ Peer dependencies:
19
+
20
+ "@angular/common": ">=22.0.0",
21
+ "@angular/core": ">=22.0.0",
22
+ "rxjs": ">=7.8.0"
23
+
24
+ Nominatim is called through `HttpClient`, so the application must provide it —
25
+ `provideHttpClient()` in the bootstrap providers, or `HttpClientModule` on a
26
+ module-based application.
27
+
28
+ ## Configuration
29
+
30
+ Everything is declared in the application `providers`, with
31
+ `provideNgxGeocoding`:
32
+
33
+ ```typescript
34
+ import { provideNgxGeocoding } from "@geosdi/ngx-geocoding";
35
+
36
+ bootstrapApplication(AppComponent, {
37
+ providers: [
38
+ provideHttpClient(),
39
+ provideNgxGeocoding({
40
+ sources: ["google", "nominatim"],
41
+ country: "it",
42
+ bias: { lat: 40.83, lon: 14.11, radiusM: 25_000 },
43
+ }),
44
+ ],
45
+ });
46
+ ```
47
+
48
+ | option | type | default | used by |
49
+ | -------------- | ------------------------------ | ----------------------- | ------------------------------ |
50
+ | `sources` | `("google" \| "nominatim")[]` | `["nominatim"]` | the order sources are tried in |
51
+ | `country` | `string` — two letters, `"it"` | none | `google` only |
52
+ | `bias` | `{ lat, lon, radiusM }` | none | `google` only |
53
+ | `nominatimUrl` | `string` | public OSM `search.php` | `nominatim` only |
54
+
55
+ `bias` is a **bias, not a filter**: an address outside the area is still
56
+ reachable, but "via Roma" proposes the nearby one first. Without it Google
57
+ orders as it likes, and on a common street name the first suggestion is almost
58
+ always somewhere else.
59
+
60
+ `provideNgxGeocoding` is optional: **not calling it leaves everything as it
61
+ was** — one source, Nominatim.
62
+
63
+ ### Nominatim
64
+
65
+ Nothing to configure, no key, no Cloud project.
66
+
67
+ ```typescript
68
+ provideNgxGeocoding({
69
+ sources: ["nominatim"],
70
+ nominatimUrl: "https://nominatim.example.org/search.php",
71
+ });
72
+ ```
73
+
74
+ Two things to know:
75
+
76
+ - `country` and `bias` are **not** applied to Nominatim. The request is left
77
+ exactly as this library has always sent it (`?q=…&format=jsonv2`), because
78
+ that is the behaviour applications relied on before Google existed here;
79
+ - the public OpenStreetMap endpoint is a courtesy service with a usage policy
80
+ (roughly: one request per second, no bulk traffic). For anything more than
81
+ occasional searches point `nominatimUrl` at your own instance.
82
+
83
+ Nominatim results **already carry `lat` and `lon`**.
84
+
85
+ ### Google
86
+
87
+ Two things live outside this library.
88
+
89
+ **1. The SDK tag**, in `index.html`, with the application's own key:
90
+
91
+ ```html
92
+ <script
93
+ defer
94
+ src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY&libraries=places&language=it"
95
+ ></script>
96
+ ```
97
+
98
+ - **the key belongs to the application** and must not be passed to this
99
+ library: there is no option for it. Loading the SDK here too would also mean
100
+ two copies of it in the same page;
101
+ - **`libraries=places`** — without it only the third step of the cascade
102
+ (`Geocoder`) can answer;
103
+ - **`language`** decides the language of the suggestions: the library passes no
104
+ language of its own to the SDK, so the one in the script tag is used;
105
+ - **`defer` is fine**: the library waits for the SDK up to 10 s, polling every
106
+ 100 ms, so a search typed right after startup does not get lost. Without
107
+ `libraries=places` the wait happens twice, once per Places step, so the first
108
+ search reaches the `Geocoder` only after about 20 s.
109
+
110
+ **2. The Google Cloud project**, where the key must have:
111
+
112
+ - **Maps JavaScript API** enabled — this is the SDK itself;
113
+ - **Places API (New)** and/or **Places API** (legacy) enabled, for the
114
+ suggestions. Either one is enough, see the cascade below;
115
+ - **Geocoding API** enabled, for the last step;
116
+ - if the key has **HTTP referrer restrictions**, every origin the application
117
+ is deployed on must be listed. A missing one answers `REQUEST_DENIED`, which
118
+ looks exactly like a product that was never enabled.
119
+
120
+ #### Which Google API actually answers
121
+
122
+ `google` is **a cascade**, and it is worth knowing before "simplifying" that
123
+ code: from a browser only **Places (New)** exposes CORS, so a plain `fetch` to
124
+ the other Google APIs cannot work at all. Going through the SDK, search works
125
+ with whatever the Cloud project has already enabled:
126
+
127
+ 1. **`AutocompleteSuggestion`** (Places **New**) — the way Google recommends
128
+ since 1 March 2025, and the only one offered to new projects. The product
129
+ behind it must be enabled separately: where it is not, it answers `403` and
130
+ the cascade steps down;
131
+ 2. **`AutocompleteService`** (Places **legacy**) — same incremental
132
+ suggestions. Google calls it deprecated and "not available to new
133
+ customers", but it is what answers on projects where the New one was never
134
+ enabled;
135
+ 3. **`Geocoder`** — no incremental suggestions, usually a single result, but it
136
+ **already carries the coordinates** and lives in the base SDK, so it does
137
+ not need `libraries=places`.
138
+
139
+ Steps go down on their own and **are not retried for the rest of the session**;
140
+ reloading the page resets that. Each fallback is written to the console with the
141
+ reason (`REQUEST_DENIED`, `OVER_QUERY_LIMIT`, …).
19
142
 
20
143
  ## Usage
21
144
 
22
- ```typescript
23
-
24
- import { Injectable } from '@angular/core';
25
- import {NgxGeocodingService} from "./ngx-geocoding.service"; import {GeocodingLocation} from "./geocoding-location.model";
26
-
27
- @Injectable({
28
- providedIn: 'root'
29
- })
30
- export class Service {
31
-
32
-
33
- constructor(private geocodingService: NgxGeocodingService,) {
34
- }
35
-
36
- public addPlugin() {
37
- this.geocodingService.geocode(addressToSearch).subscribe((res: GeocodingLocation[]) => {
38
- alert(JSON.stringify(res));
145
+ ```typescript
146
+ import { Component, inject } from "@angular/core";
147
+ import { GeocodingLocation, NgxGeocodingService } from "@geosdi/ngx-geocoding";
148
+
149
+ @Component({ selector: "app-search", standalone: true, template: "…" })
150
+ export class SearchComponent {
151
+ private geocodingService: NgxGeocodingService = inject(NgxGeocodingService);
152
+
153
+ places: GeocodingLocation[] = [];
154
+ chosen?: GeocodingLocation;
155
+
156
+ search(address: string): void {
157
+ this.geocodingService.geocode(address).subscribe({
158
+ // An empty list means "nothing found", and also fires subjectNoResults.
159
+ next: (res: GeocodingLocation[]) => (this.places = res),
160
+ // The error means "I could not search at all": no source was usable.
161
+ error: () => (this.places = []),
162
+ });
163
+ }
164
+
165
+ // Google suggestions arrive WITHOUT coordinates: resolve() asks for them,
166
+ // for the chosen place only. Places that already have them (Nominatim, the
167
+ // Geocoder) are returned as they are, so calling it is always correct.
168
+ choose(place: GeocodingLocation): void {
169
+ this.geocodingService
170
+ .resolve(place)
171
+ .subscribe((full: GeocodingLocation) => {
172
+ this.chosen = full;
173
+ if (full.lat && full.lon) {
174
+ // Number(undefined) would be NaN: coordinates may be missing if the
175
+ // detail request failed, and the place is returned as it was.
176
+ this.moveMap(Number(full.lat), Number(full.lon));
177
+ }
39
178
  });
40
- }
179
+ }
41
180
 
181
+ private moveMap(lat: number, lon: number): void {
182
+ // the application's own: for example map.setView([lat, lon], 16)
183
+ }
42
184
  }
185
+ ```
43
186
 
44
- ```
187
+ `geocode` **errors** when no source could be used, and **emits an empty list**
188
+ when the search simply found nothing: "I found nothing" and "I could not
189
+ search" are two different things, and a search bar has to tell them apart.
45
190
 
46
191
  ## Methods
47
192
 
48
- | Method | action
49
- | ------------- | ------------- |
50
- | geocode(address: string): Observable<GeocodingLocation[]> | list of GeocodingLocation |
193
+ | Method | action |
194
+ | ------------------------------------------------------------------ | -------------------------------------------------------------------- |
195
+ | `geocode(address: string): Observable<GeocodingLocation[]>` | suggestions for the typed text, from the first source that answers |
196
+ | `resolve(place: GeocodingLocation): Observable<GeocodingLocation>` | coordinates of the **chosen** place; returns it unchanged on failure |
51
197
 
52
- ## Events
198
+ ## Model
53
199
 
54
- | Method | action
55
- | ------------- | ------------- |
56
- | subjectNoResults | subscribe to notify if research is empty |
57
- | subjectLocation | subject to notify location |
58
- | subjectRemove | subject to notify remove location |
200
+ | `GeocodingLocation` | content |
201
+ | ------------------- | ---------------------------------------------------------- |
202
+ | `display_name` | the address to show |
203
+ | `lat`, `lon` | coordinates, as strings — **absent on Google suggestions** |
204
+ | `place_id` | id of the place, what `resolve` needs |
205
+
206
+ ## Events
59
207
 
208
+ | Method | action |
209
+ | ------------------ | ---------------------------------------- |
210
+ | `subjectNoResults` | subscribe to notify if research is empty |
211
+ | `subjectLocation` | subject to notify location |
212
+ | `subjectRemove` | subject to notify remove location |
213
+
214
+ Only `subjectNoResults` is emitted by the library. `subjectLocation` and `subjectRemove` are
215
+ channels for the application: nothing in the library calls `next` on them.
216
+
217
+ ## Troubleshooting
218
+
219
+ Every fallback is logged with `console.warn`, prefixed `[ngx-geocoding]`.
220
+
221
+ | what you see | what it means |
222
+ | ------------------------------------------- | ---------------------------------------------------------------------------------- |
223
+ | `SDK Maps senza la libreria 'places'` | `libraries=places` missing from the script tag, or no SDK in the page; ~20 s wait |
224
+ | `SDK Maps senza il Geocoder` | no SDK in the page at all |
225
+ | `REQUEST_DENIED` | product not enabled on the Cloud project, or referrer outside the key restrictions |
226
+ | `OVER_QUERY_LIMIT` | quota or billing on the Google project |
227
+ | `sorgente "google" non utilizzabile` | every Google step refused; the cascade fell through to the next source |
228
+ | `Nessuna sorgente di geocoding disponibile` | all declared sources refused — this is the error `geocode` throws |
229
+
230
+ A source that refused is not asked again until the page is reloaded, so a fix on
231
+ the Cloud console needs a reload to be seen.
232
+
233
+ **Any error counts as a refusal, including a transient one.** A request that fails
234
+ because the network dropped for a moment, or a `429` from the public Nominatim
235
+ (easy to get with search-as-you-type), switches that source off for the session
236
+ just like a `REQUEST_DENIED` does. With `sources: ["nominatim"]` — the default —
237
+ every following search then fails with `Nessuna sorgente di geocoding disponibile`
238
+ until the page is reloaded. Debounce the input, and keep more than one source
239
+ where you can.
240
+
241
+ ## Changes in 2.0.0
242
+
243
+ - **A major version** because the peer dependencies now ask for Angular 22:
244
+ an application on an older Angular can no longer install it. The API is
245
+ unchanged unless a point below says otherwise.
246
+ - `typescript` is no longer a peer dependency: nothing in the package
247
+ imports it.
248
+ - **The Angular peer dependencies are `>=22.0.0`**: the library is compiled and tested with
249
+ Angular 22 only, so it no longer claims to install on older versions.
250
+ - Compiled with Angular 22. The peer dependencies were already correct and are unchanged.
251
+ - This README: the title of the package, which events the library emits, and what a transient
252
+ error does to a source.
253
+ - The code is unchanged.