JPS Explorer is an iOS/Android app for browsing over 32 million Japanese cultural heritage items through the Japan Search (jpsearch.go.jp) Web API. This article covers what was learned during API investigation, app implementation with Flutter, and automating the App Store release process.

Japan Search API

Japan Search is operated by the National Diet Library of Japan and provides cross-search access to metadata for over 32 million digital cultural resources. A public Web API supports the following search parameters:

ParameterFunction
keywordKeyword search
text2imageImage search by text-based motif description
imageSimilar image search using an existing item ID
g-coordinatesLocation search by latitude, longitude, and radius
r-tempoEra search by year range

Findings from API investigation

Coordinate field key name

In the response from location-based search (g-coordinates), coordinate data is stored in common.coordinates. The longitude key is lon, not lng or longitude.

"coordinates": {
  "lat": 35.669,
  "lon": 139.764
}

The gallery search endpoint (/api/curation/search) returns title and summary as objects rather than strings.

"title": {"ja": "耳鳥斎", "en": "Jichosai"},
"image": {"url": "https://...", "thumbnailUrl": "https://..."}

Calling .toString() directly would display something like {ja: 耳鳥斎, en: Jichosai} in the UI.

In the gallery detail endpoint (/api/curation/{id}), items are nested in a parts array rather than contents. Items of type jps-curation-list-item need to be collected by recursively traversing the structure. Some galleries also contain items in subPages.

Image upload for similar search (undocumented API)

The official API guide does not document image-upload-based similarity search, but inspecting the Web UI’s network traffic revealed a three-step process:

  1. POST /dl/api/imagefeatures/ – sends Base64 image, returns a 64-dimensional feature vector
  2. POST /api/item/create-image-feature – sends feature vector, returns a temporary search ID
  3. GET /api/item/search/jps-cross?image={ID} – standard similar image search

Step 2 requires the X-Requested-With: XmlHttpRequest header. The response is a plain-text ID.

This mechanism makes it possible to search Japan Search using a photo taken with the device camera. Further details are covered in a separate article.

Rights and database name ID resolution

Search results contain code values like pdm and ccby in common.contentsRightsType. Fields such as common.database and common.provider are also IDs. Displaying human-readable labels requires separate calls to /api/database/{id} and /api/organization/{id}, with results cached locally.

App implementation

Technology stack

  • Flutter + Riverpod (state management)
  • flutter_map + OpenStreetMap (map display)
  • CachedNetworkImage (image caching)
  • Japanese/English localization

Mobile-specific features

The following features take advantage of mobile capabilities beyond what the browser-based Japan Search offers:

FeatureAPI usedMobile technology
Camera-based similar searchUndocumented API (imagefeatures)Camera / image_picker
Nearby cultural resource notificationsg-coordinatesBackground location monitoring + local notifications
Map explorationg-coordinatesflutter_map + Geolocator
Offline favoritesLocal storage + image cache
Spotlight integrationCoreSpotlight (iOS MethodChannel)
Haptic feedbackHapticFeedback
Today’s Pickkeyword (random offset)SharedPreferences for daily cache

Nearby notifications

The app monitors location in the background and triggers a Japan Search API query when the user moves more than 500 meters. If new cultural resources are found nearby, a local notification is sent. Battery usage is kept low by using LocationAccuracy.low and distanceFilter: 500. Duplicate notifications are suppressed by keeping the most recent 100 item IDs in SharedPreferences.

Spotlight integration

To index items in iOS CoreSpotlight, the app calls native Swift code through a MethodChannel. The implementation was placed directly in AppDelegate rather than in a separate file (e.g., SpotlightPlugin.swift), because adding a separate Swift file requires updating the Xcode project file (.pbxproj), which is difficult to automate from the command line.

Tip feature

The app includes a Tip Jar powered by StoreKit for in-app purchases, implemented in tip_jar_service.dart.

App Store release automation

Automated tasks

The script scripts/release.py handles the following from the command line:

python3 scripts/release.py build      # Build and upload
python3 scripts/release.py screenshots # Capture screenshots
python3 scripts/release.py submit      # Set metadata and submit for review
TaskMethod
Bundle ID registrationPOST /v1/bundleIds
Build and uploadflutter build ipa then xcrun altool --upload-app
Metadata configurationPATCH /v1/appStoreVersionLocalizations/{id}
Screenshot capturexcrun simctl io screenshot + Pillow for marketing images
Screenshot uploadThree-step process (reserve, upload binary, commit)
Age ratingPATCH /v1/ageRatingDeclarations/{id}
Category and copyrightPATCH /v1/appInfos/{id}, PATCH /v1/appStoreVersions/{id}
Build associationPATCH /v1/appStoreVersions/{id}/relationships/build
Encryption compliancePATCH /v1/builds/{id}
Review detailsPOST /v1/appStoreReviewDetails
Review submissionPOST /v1/reviewSubmissions + reviewSubmissionItems

Tasks that could not be automated

TaskReason
App creationThe OpenAPI spec only defines GET for /v1/apps. The API error message confirms: “CREATE is not allowed. Allowed operations are: GET_COLLECTION, GET_INSTANCE, UPDATE”
App Privacy (data usage declarations)A thorough search of the OpenAPI spec confirmed no endpoints for appDataUsages, appDataUsageCategories, or any privacy-related paths in v1 or v2
In-App Purchase product registrationInitial product creation must be done through the browser

For App Privacy, the official OpenAPI JSON (openapi.oas.json) was searched exhaustively for keywords such as privacy, dataUsage, consent, and declaration, confirming that no corresponding endpoints exist.

Configuration via .env

API keys and contact information are loaded from a .env file rather than being hardcoded. A .env.example template is provided in the repository with placeholder values.

Release pitfalls

Settings that are easy to miss

Even after configuring all fields via the API, the following tend to be overlooked. A pre-submission check script is recommended.

FieldAPINotes
Promotional TextpromotionalText in appStoreVersionLocalizationsOptional but recommended. Can be changed at any time without review
Marketing URLmarketingUrl in appStoreVersionLocalizationsOptional
Privacy URL (English)appInfoLocalizationsEasy to accidentally set the same URL as the Japanese version. Needs the English-language path
PricingappPriceSchedulesMust be explicitly set even for free apps. Use appPricePoints to get the FREE (price=0) ID

whatsNew is unavailable for the first version

Setting whatsNew (release notes) on the initial release returns a 409 STATE_ERROR. This field is only available from the second version onward.

Phone number format for review details

The contactPhone field in review details requires +country_code-number format. Placeholder numbers are rejected as invalid. A reliable approach is to retrieve contact details from an existing app’s review detail via GET /v1/appStoreVersions/{id}/appStoreReviewDetail and reuse them.

Screenshot capture and marketing images

Flutter integration_test is used to automatically capture screenshots of each screen. The test taps ActionChip and NavigationBar icons to navigate through five screens: explore, search results, detail, map, and gallery.

final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ... app.main() -> pumpAndSettle -> takeScreenshot

The simulator language is switched between Japanese and English, and tests are run on both iPhone and iPad simulators, producing 5 screens x 2 languages x 2 devices = 20 screenshots.

Marketing images are generated using a Python script based on a reference project. Key aspects include:

  • SCREENSHOT_PRIORITY controls which screenshot maps to which theme. Matching is done by filename prefix (e.g., 02_search, 05_gallery)
  • The count parameter in find_best_screenshots() must match the number of themes
  • bleed_fraction = 0.35 creates a layout where the bottom 35% of the device bleeds off-screen
  • Font sizes and screenshot scaling automatically adjust for iPhone (aspect ratio ~0.46) vs iPad (~0.75)
  • Separate --input-iphone and --input-ipad flags allow using resolution-optimized images for each device type

Screenshots cannot be changed after submission

Once an app is submitted for review (Waiting For Review status), screenshots cannot be deleted or added. Screenshot quality should be verified before submission.

Screenshot language considerations

When capturing Flutter app screenshots automatically, the simulator’s language setting determines the app’s UI language. Taking Japanese screenshots requires switching the simulator to Japanese. Reusing identical screenshots across multiple slots may lead to rejection during review. Adding a hash-based duplicate detection check is advisable.

Project structure

jps_explorer/
├── lib/
│   ├── models/jps_item.dart
│   ├── services/
│   │   ├── jps_api_service.dart      # JPS API client
│   │   ├── label_service.dart        # DB/org/rights label resolution
│   │   ├── daily_pick_service.dart   # Daily random item
│   │   ├── nearby_notification_service.dart
│   │   ├── image_cache_service.dart  # Offline images
│   │   ├── spotlight_service.dart    # iOS Spotlight
│   │   ├── favorites_service.dart
│   │   └── tip_jar_service.dart      # Tip
│   ├── views/                        # Screen widgets
│   └── providers/app_providers.dart  # Riverpod
├── scripts/
│   ├── release.py                    # Build and submission automation
│   ├── capture_screenshots.sh        # Screenshot capture
│   └── generate_marketing_screenshots.py  # Marketing image generation
├── .env                              # Configuration (gitignored)
└── .env.example                      # Configuration template