# Photo Tagging Coordinate Model
When users tag people in photos, store positions relative to the image, not as absolute pixels.
Absolute coordinates break when the same image is served as multiple responsive variants.
## Point Tags
Bad:
```text
x = 600
y = 400
```
This only works for one exact image size.
Good:
```text
x_ratio = x / original_width
y_ratio = y / original_height
```
Example:
```text
original = 1024 x 768
tag pixel = 600, 400
x_ratio = 0.586
y_ratio = 0.521
```
If the image is served as `512 x 384`, render the marker at:
```text
x = 0.586 * 512
y = 0.521 * 384
```
## Bounding Boxes
For a face/object rectangle, store either:
```text
x1, y1, x2, y2
```
or:
```text
x, y, width, height
```
All values should be normalized ratios from `0.0` to `1.0`.
```mermaid
flowchart LR
Original[Original photo] --> Normalize[Store normalized box]
Normalize --> Thumb[Thumbnail render]
Normalize --> Mobile[Mobile render]
Normalize --> Desktop[Desktop render]
```
## Schema
```sql
photo_people_tags (
id bigint primary key,
photo_id bigint not null,
tagged_user_id bigint not null,
created_by_user_id bigint not null,
x1 float not null,
y1 float not null,
x2 float,
y2 float,
created_at timestamp not null
)
```
For point-only tags, omit `x2/y2` or store a small default box around the point.
## Coordinate Spaces
Be explicit about which image the ratios refer to:
| Space | Use Case | Risk |
|---|---|---|
| Original upload | Stable archival coordinates | Later crop changes visible placement |
| Cropped canonical image | Matches current product display | Needs transform if crop changes |
| ML model input image | Useful for detection pipeline | May not match user-visible pixels |
| Rendered viewport | Easy for UI | Wrong after resize, rotation, or DPR changes |
The API should accept the client-visible point plus the displayed image dimensions or crop metadata. The server can then normalize into the canonical space.
## Rendering Flow
```text
display_x = (tag_x - crop_x) / crop_width * rendered_width
display_y = (tag_y - crop_y) / crop_height * rendered_height
```
Clamp markers to the visible image area. A tag outside the current crop should be hidden, moved to an edge affordance, or require a crop transform.
## Product Rules
Design questions that affect storage:
- Can users tag people who are not in the app?
- Can tagged users approve/remove tags?
- Are tags public, follower-only, or private?
- Do tags survive image crop edits?
- Are face boxes generated by ML and then user-confirmed?
- Can one photo have hundreds of tags?
## Moderation And Privacy
Treat a tag as both geometry and a social edge. A row may need status:
```text
pending, approved, rejected, removed_by_tagged_user, removed_by_moderator
```
Notification timing follows that status. For example, a private account may require approval before the tag appears on the public photo or creates a profile backlink.
## Senior-Level Gotcha
If the user can crop an image after tagging, normalized coordinates relative to the original may still drift. Store which image coordinate space the tag belongs to:
- original image,
- cropped image,
- current canonical display image.
Then transform coordinates when rendering variants.
## Failure Modes
- EXIF rotation is applied in one service but not another. Normalize orientation before storing dimensions.
- Client rounds ratios too aggressively. Store enough precision for large images.
- Face detection creates overlapping tags. Keep user-confirmed tags separate from ML suggestions.
- Deleted user/profile still appears in old photo metadata. Hydrate tags through user privacy checks.