# Positions

The GPS fixes of where a logged group was fired from and where its target stood, with their accuracy and the two kinds of height a fix can carry.

## Generated table

**Record:** observation · **Covers:** `positions`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `positions.firing` | object | no | - | no | removed when location is hidden |
| `positions.firing.accuracy_m` | number | no | min 0 | no | removed when location is hidden |
| `positions.firing.elevation_ft` | number | no | - | no | removed when location is hidden |
| `positions.firing.elevation_source` | `'map'` or `'manual'` or `'estimated'` | no | - | no | removed when location is hidden |
| `positions.firing.gps_altitude_acc_m` | number | no | - | no | removed when location is hidden |
| `positions.firing.gps_altitude_m` | number | no | - | no | removed when location is hidden |
| `positions.firing.lat_deg` | number | yes, within | min -90; max 90 | no | removed when location is hidden |
| `positions.firing.lon_deg` | number | yes, within | min -180; max 180 | no | removed when location is hidden |
| `positions.firing.source` | `'phone'` or `'manual'` or `'estimated'` | yes, within | - | no | removed when location is hidden |
| `positions.target` | object; a GPS fix, with the same fields as `positions.firing` | no | - | no | removed when location is hidden |

## What it holds

The `positions` block holds up to two GPS fixes for a logged group: where it was fired from (`firing`) and where the target stood (`target`). Either can be there alone. A group with neither has no `positions` key. Each fix has a latitude, a longitude, a source word, and optionally an accuracy and two heights. It is a copy made when the group was saved.

## What to know first

**Two stored heights differ.** `elevation_ft` is ground height above sea level. `gps_altitude_m` is the phone's own altitude as reported: one phone family measures from the ellipsoid, which reads tens of metres lower than sea-level height in the US, the other from sea level. Neither is corrected.

**No calculation reads the positions.** The Session screen uses them for the next form and a bearing, and the CSV export writes latitude, longitude and accuracy.

**Voice never writes them, but can read them.** `log_group` has no input for a position. `get_session` returns a stored block whole.

**Both points carry over.** After "Add group" the form keeps the firing and target points, so the next group gets the same target position at any range.

**Location shown gives every reader the whole fix, the public included.** Hidden removes the block, for every audience.

## The fields

**`positions`** The block. It holds `positions.firing` and `positions.target`, both optional.

**`positions.firing`** The fix of the firing point, with the members below. The log form fills it with "Use phone GPS" or from typed values.

**`positions.target`** The fix of the target. It has the same members as the firing point, but the log form can fill only the latitude, longitude, source, elevation and elevation source. It is typed, or computed with "Estimate from range + azimuth". The form has no GPS button for it.

Both points are made of these members (the paths below are the firing point's; the target's are the same under `positions.target`).

**`positions.firing.lat_deg`** Latitude in degrees, positive north, in the WGS84 datum. Required. The form allows -90 to 90. The GPS button writes 5 decimals (about 1 m). Example: 12.34567.

**`positions.firing.lon_deg`** Longitude in degrees, positive east, WGS84. Required. The form allows -180 to 180. Example: -23.45678.

**`positions.firing.source`** How the point was made: `phone` (a GPS fix), `manual` (typed, for example read off a map) or `estimated` (computed). Required. The form writes `phone` only through the GPS button, and `manual` as soon as the latitude or longitude is typed. The app writes `estimated` only for a target point.

**`positions.firing.accuracy_m`** The horizontal accuracy the phone reported, in metres, rounded to a whole metre. Optional. Only a GPS fix has it. Typing the latitude or longitude clears it. A stored 0 is kept as 0; a negative or unreadable value is dropped by the form. The form has no accuracy for the target point.

**`positions.firing.elevation_ft`** The ground height above sea level in feet. Optional. Negative values are fine. It comes from a terrain map lookup at the point (a US survey service, else a world terrain model), from typing, or, for an estimated target, from the firing point's height and the shot. Example: 400.

**`positions.firing.elevation_source`** Where `elevation_ft` came from: `map`, `manual` or `estimated`. Optional, and present with the height. Typing a ground elevation sets `manual`. A fix typed afterwards clears a `map` height but keeps a typed one.

**`positions.firing.gps_altitude_m`** The phone's altitude in metres exactly as it reported it, to 0.1 m. Optional, firing point only in the form. It is kept so the two phone families can be told apart and the ground height checked. Typing a latitude or longitude clears it.

**`positions.firing.gps_altitude_acc_m`** The altitude's accuracy in metres, to a whole metre. Optional. The form keeps it only together with an altitude.

A third height is in play on the screen and is not stored: the weather lookup reports its own grid height, which the Weather button's note shows in feet. See [air](air.md).

**What the target's fix is for.** An estimated target is computed on a sphere: it goes the range times the cosine of the slope along the azimuth from the firing point. Its height is the firing point's height plus the range times the sine of the slope plus a small Earth-curvature term. The app does the sum once, in the form, and stores the answer. For a made-up shot of 1,760 yd due east, 6 degrees downhill, from ground at 400 ft, the estimate is 1,750.4 yd away horizontally and 151 ft below sea level. The planned target of a session has the same kind of fix in its `position` (see [session targets](session-targets.md)).

**Limits, by writer.** The block has no connector input and no conversion step, so the limits are listed. Units are the field's own.

- App, the log form: latitude and longitude are both given or both blank (one alone is refused with "Missing or invalid: firing point latitude/longitude", and the group is not saved). Each must be a number, the latitude within 90 and the longitude within 180 degrees, the end values included. A ground elevation that is not a number is refused the same way; any plain number is accepted (-99999 ft is saved; an exponent such as 1e30 is refused). A negative or unreadable accuracy is dropped without a message, and so is a raw altitude that is not a number. An altitude accuracy without an altitude is dropped.
- Stored record, checked when the group syncs: each number must be a number, the two source words one of their allowed words, and no other member is allowed. Since 2026-10-24 the schema holds a latitude to -90 to 90, a longitude to -180 to 180 and an accuracy to 0 or more (both the app's check and the server's use it); before, a latitude of 91, a longitude of 999 and an accuracy of -5 all passed. No other range is checked. An empty `positions` object, an elevation source with no elevation, and an accuracy or raw altitude on the target pass too.
- Connector: none; it cannot write the block.
- Solve and fit: none; nothing reads it.

## Who writes it

**The app.** Only the Session screen's group form writes it. "Use phone GPS" asks the phone for a high-accuracy fix (it waits up to 30 seconds), fills the firing point with source `phone`, the accuracy and the raw altitude, and fills the air's latitude too when that is blank. It then looks up the ground height online (the lookup sends the point's latitude and longitude to an outside terrain service) and fills `elevation_ft` with source `map`. Offline, or when the lookup fails, it leaves the height as it was and says to type it: a height looked up at an earlier fix stays, still marked `map`. Typing the latitude or longitude of the firing point makes the source `manual` and clears the accuracy, the raw altitude and a `map` height. The target point is typed (read off a map, or a fix taken at the target and typed in) or computed: "Estimate from range + azimuth" needs the firing point, range, azimuth and slope, and stores source `estimated`. Its height starts from the firing point's ground height, else a map lookup of the firing point, else the raw GPS altitude (the message says so), else none. Typing the target's latitude or longitude makes it `manual` and drops its height.

The form starts from the session's saved draft, else from the newest group's block, and keeps both points after "Add group". "Log this shot" in the Calculator keeps the firing point and clears the target point. A block is saved only when a latitude and longitude are typed for that point.

"Use azimuth" reads the bearing between the two points (a great circle, true north) and copies only the bearing into the azimuth field. The distance is shown and not copied. The button is hidden for an estimated target, which was made from the azimuth.

**The connector.** Nothing writes it. `log_group` takes no position and its pushed record has no `positions` key (run with the test backend). If it writes a place at all, it is the phone's latitude in the notes (see [air](air.md)), and that is not this block.

**Never fitted.** No fit writes the block. Since 2026-10-13 the app can edit a saved group, its two points included (what an edit keeps and works out again is on the [group](group.md) page). For a copy or a restore of a group, see [group record](observation.md).

## Who reads it

**The app.** The Session screen reads the newest group's block to start the next form, and the two points to show the bearing and distance. The CSV export writes `firing_lat`, `firing_lon`, `firing_acc_m`, `target_lat` and `target_lon` for each group; the heights, the source words and the target's accuracy are not exported. The log table shows no position.

**The fitter and the solver.** None. The azimuth, the slope and the range of a group are the shooter's own entries; nothing derives them from the points. The air's latitude comes from the form's own field, which the GPS button fills when it is blank. No guide, chart or pooled fit reads the block.

**The connector.** `get_session` returns each group as stored, so both points reach the assistant whole, with their accuracy and heights. No tool uses them to solve or to log, and no spoken line states them.

**The backup.** The JSON backup holds the block whole.

**The share filter.** It removes the block whole or passes it whole (see "Sharing and privacy").

No solver option is named in the front matter: no option reads a position.

## Why it is stored

The points record where a group was fired and where it was aimed, as the shooter knew it on the day, apart from any calculation. The accuracy says how much to trust a fix. The ground height and the raw altitude are kept apart on purpose: the phone's altitude differs by phone family and is not the ground height, so the app records both and corrects neither. The estimated source marks a target point that nobody looked up. The code and the specs say little more: no reader needs the block to solve or fit, so it is a record for the shooter and for later work.

## Changing it

The record is not versioned: a group is added, edited in place on the Session screen (since 2026-10-13), or deleted by setting its deleted mark. A deleted group's points go with it from every view. The edit category is "none": nothing here changes a calibration. A profile edit does not touch a group's points.

## Sharing and privacy

The share has one location switch, and the block follows it for every audience. The switch is the same for the public, one person and the admins.

| Location | What a reader of the shared log gets |
|---|---|
| Shown | The whole block as stored: `positions.firing` and `positions.target`, each at full precision with its accuracy, source word and both heights. |
| Hidden | No `positions` key: none of `positions.firing.lat_deg`, `positions.firing.lon_deg`, `positions.firing.accuracy_m`, `positions.firing.source`, `positions.firing.elevation_ft`, `positions.firing.elevation_source`, `positions.firing.gps_altitude_m`, `positions.firing.gps_altitude_acc_m` or `positions.target` reaches the reader. |

A new share with one person or with the admins starts with location shown. A public share starts with it hidden. The owner can change either.

With location hidden, a reader still gets the range and slope of each group and its direction to the nearest 10 degrees ([target](target.md)), and the station pressure to 0.1 inHg with a latitude to the whole degree ([air](air.md)); no photo is shared ([photo](photo.md)). From these a reader can work out the distance of each shot and, roughly, its direction and how high the range lies. With location shown, a reader can place the firing point to within the fix's accuracy, find the ground height there, and tell the two phone families apart by the raw altitude. The target's point, when it is estimated, only repeats what the range and azimuth already say. Notes follow their own switch and are not changed by this one ([group record](observation.md)).

---

[All reference pages](../index.md)
